<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Speech-to-Text | Bas Nijholt</title><link>https://www.nijho.lt/tag/speech-to-text/</link><atom:link href="https://www.nijho.lt/tag/speech-to-text/index.xml" rel="self" type="application/rss+xml"/><description>Speech-to-Text</description><generator>Hugo</generator><language>en-us</language><copyright>© 2026</copyright><lastBuildDate>Tue, 22 Sep 2026 00:00:00 +0000</lastBuildDate><image><url>https://www.nijho.lt/media/icon_hu_408a8f6cef43e327.png</url><title>Speech-to-Text</title><link>https://www.nijho.lt/tag/speech-to-text/</link></image><item><title>Frontier-level dictation on your iPhone keyboard, self-hosted</title><link>https://www.nijho.lt/post/diction-agent-cli-qwen/</link><pubDate>Tue, 22 Sep 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/diction-agent-cli-qwen/</guid><description>&lt;p&gt;On a recent month-long trip visiting family in Europe, I did a lot of work from my phone, using &lt;a href="https://www.nijho.lt/post/agentic-mobile-workflow/"&gt;my mobile coding workflow&lt;/a&gt;.&#10;It still boggles my mind that real, productive work from a phone is possible now.&#10;Most of that work is talking to coding agents, and the built-in iOS dictation is garbage for that.&#10;I had an iOS Shortcut that sent recordings to &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;Agent CLI&lt;/a&gt;, &lt;a href="https://www.nijho.lt/post/auto-install-extras/"&gt;my local AI toolbox&lt;/a&gt;, running at home instead.&#10;The problem was that the recording screen takes over the whole display, so I could not see the thing I was commenting on.&#10;The transcript then landed in my clipboard, and I had to paste it myself.&lt;/p&gt;&#10;&lt;p&gt;So I threw my principles overboard and installed &lt;a href="https://wisprflow.ai" target="_blank" rel="noopener"&gt;Wispr Flow&lt;/a&gt;, a voice keyboard for iOS.&#10;I loved it instantly.&#10;iOS does not let keyboards use the microphone, so the keyboard&amp;rsquo;s button hands off to the Wispr Flow app, which keeps recording in the background.&#10;I was less happy about that, since it meant a closed-source app always had the microphone open and sent my voice to their servers.&#10;I was on the free plan, so I was probably the product.&#10;I also hit the free weekly word limit very quickly, and started looking for an alternative.&lt;/p&gt;&#10;&lt;p&gt;That is how I found &lt;a href="https://apps.apple.com/app/id6759807364" target="_blank" rel="noopener"&gt;Diction&lt;/a&gt;, which has since become my favorite app when I&amp;rsquo;m away from my laptop.&#10;It works the same way, background recording included.&#10;The difference is that it has a self-hosted mode with an open-source gateway, so my voice goes to a machine I own instead of their servers.&lt;/p&gt;&#10;&lt;p&gt;I run the &lt;a href="https://github.com/DictionLabs/Diction" target="_blank" rel="noopener"&gt;gateway&lt;/a&gt; on my home machine in the U.S., reachable only over my private network.&#10;The gateway streams audio to Agent CLI, which runs Alibaba&amp;rsquo;s &lt;a href="https://huggingface.co/Qwen/Qwen3-ASR-1.7B-hf" target="_blank" rel="noopener"&gt;&lt;code&gt;Qwen/Qwen3-ASR-1.7B-hf&lt;/code&gt;&lt;/a&gt; on an RTX 3090 behind an OpenAI-compatible transcription endpoint.&#10;There is no LLM cleanup step, so Diction&amp;rsquo;s Writing Style and Tones features do not work with this setup.&#10;I don&amp;rsquo;t miss them: Qwen&amp;rsquo;s raw transcription is already good enough for me.&lt;/p&gt;&#10;&lt;h2 id="1-why-qwen"&gt;1. Why Qwen? &lt;a class="anchor" href="#1-why-qwen" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;For the longest time I used &lt;a href="https://github.com/SYSTRAN/faster-whisper" target="_blank" rel="noopener"&gt;Faster-Whisper&lt;/a&gt; with Whisper large-v3.&#10;Whisper is still the default choice for most people, but many newer models beat it.&#10;I tried some of them, including NVIDIA&amp;rsquo;s &lt;a href="https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3" target="_blank" rel="noopener"&gt;Parakeet&lt;/a&gt;.&#10;Parakeet is fast, but it cannot take custom instructions.&#10;Those instructions are how Diction&amp;rsquo;s &lt;strong&gt;My Words&lt;/strong&gt; feature biases a transcription toward specialized names.&lt;/p&gt;&#10;&lt;p&gt;At the start of September, I looked at the &lt;a href="https://huggingface.co/spaces/hf-audio/open_asr_leaderboard" target="_blank" rel="noopener"&gt;Hugging Face Open ASR Leaderboard&lt;/a&gt;, which tests speech-to-text models on the same recordings and ranks them.&#10;The score is the word error rate (WER): the share of words a model gets wrong, counting words it swaps, drops, or makes up.&#10;Lower is better, and a WER of 4% means about one wrong word in every 25.&#10;The test recordings range from audiobooks and podcasts to meetings and earnings calls, so the average says more than any single clean benchmark.&lt;/p&gt;&#10;&lt;p&gt;These are the averages on the public English test sets as of September 19, 2026:&lt;/p&gt;&#10;&lt;div class="table-wrap"&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;Model&lt;/th&gt;&#10;&lt;th&gt;WER&lt;/th&gt;&#10;&lt;th&gt;Can I run it at home?&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Zoom Scribe v2 Pro&lt;/td&gt;&#10;&lt;td&gt;3.6%&lt;/td&gt;&#10;&lt;td&gt;No, paid API (#1 overall)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;ElevenLabs Scribe v2&lt;/td&gt;&#10;&lt;td&gt;4.0%&lt;/td&gt;&#10;&lt;td&gt;No, paid API&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;Qwen3-ASR 1.7B&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;&lt;strong&gt;4.3%&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;&lt;strong&gt;Yes, Apache-2.0&lt;/strong&gt;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;AssemblyAI Universal-3.5 Pro&lt;/td&gt;&#10;&lt;td&gt;4.3%&lt;/td&gt;&#10;&lt;td&gt;No, paid API&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;NVIDIA Parakeet TDT 0.6B v3&lt;/td&gt;&#10;&lt;td&gt;4.9%&lt;/td&gt;&#10;&lt;td&gt;Yes&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;OpenAI Whisper large-v3&lt;/td&gt;&#10;&lt;td&gt;5.8%&lt;/td&gt;&#10;&lt;td&gt;Yes&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;I picked the best model on the board whose weights you can download: Qwen3-ASR 1.7B.&#10;It lands within a point of the best paid API, which is what I mean by frontier-level.&#10;Since I maintain Agent CLI, I told an agent to add a Qwen backend, and shortly afterward &lt;a href="https://github.com/basnijholt/agent-cli/pull/636" target="_blank" rel="noopener"&gt;it had landed&lt;/a&gt; and was running on my server.&lt;/p&gt;&#10;&lt;p&gt;Agent CLI&amp;rsquo;s transcription server loads a model on the first request and unloads it after an idle timeout, so the model only takes VRAM while I use it.&#10;The same process serves an OpenAI-compatible API, which the Diction gateway talks to, and the &lt;a href="https://www.home-assistant.io/integrations/wyoming/" target="_blank" rel="noopener"&gt;Wyoming protocol&lt;/a&gt; that Home Assistant uses for local voice.&lt;/p&gt;&#10;&lt;h2 id="2-the-recipe"&gt;2. The recipe &lt;a class="anchor" href="#2-the-recipe" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;You need an NVIDIA GPU with the &lt;a href="https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html" target="_blank" rel="noopener"&gt;NVIDIA Container Toolkit&lt;/a&gt;, Docker Compose 2.30 or newer, and a private route from your iPhone to the server.&#10;I use Headscale, but Tailscale or plain WireGuard works just as well.&#10;Do not expose the gateway to the public internet.&lt;/p&gt;&#10;&lt;h3 id="write-the-compose-file"&gt;Write the Compose file &lt;a class="anchor" href="#write-the-compose-file" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Save the following as &lt;code&gt;compose.yaml&lt;/code&gt; in an empty directory.&#10;Agent CLI&amp;rsquo;s CI publishes a CUDA image with the Transformers backend and Qwen as the default model, so there is nothing to build.&#10;The file points Diction&amp;rsquo;s gateway at Agent CLI&amp;rsquo;s transcription port.&#10;&lt;code&gt;WHISPER_TTL&lt;/code&gt; raises the idle timeout from the default five minutes to a day, so dictation rarely waits for a reload:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;asr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ghcr.io/basnijholt/agent-cli-whisper:0.112.0-cuda-transformers&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;WHISPER_TTL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;86400&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;model-cache:/home/whisper/.cache&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;gpus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;all&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;dictionlabs/gateway:v14.0&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;depends_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;asr&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;CUSTOM_BACKEND_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;http://asr:10301&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;CUSTOM_BACKEND_MODEL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Qwen/Qwen3-ASR-1.7B-hf&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;CUSTOM_BACKEND_CANONICAL_ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Qwen/Qwen3-ASR-1.7B-hf&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;CUSTOM_BACKEND_NEEDS_WAV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;true&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;8080:8080&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;model-cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="start-it-and-connect-the-app"&gt;Start it and connect the app &lt;a class="anchor" href="#start-it-and-connect-the-app" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Start both services:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first transcription downloads Qwen into the &lt;code&gt;model-cache&lt;/code&gt; volume, so that request can take a few minutes.&#10;&lt;code&gt;docker compose logs -f&lt;/code&gt; shows the progress.&lt;/p&gt;&#10;&lt;p&gt;On the iPhone, go to &lt;strong&gt;Settings → General → Keyboard → Keyboards → Add New Keyboard → Diction&lt;/strong&gt;.&#10;Tap Diction in the keyboard list, enable &lt;strong&gt;Allow Full Access&lt;/strong&gt;, and grant microphone access when prompted.&#10;Then open Diction, go to &lt;strong&gt;Preferences → Mode → Self-Hosted&lt;/strong&gt;, enter &lt;code&gt;http://&amp;lt;private-server-ip&amp;gt;:8080&lt;/code&gt;, and tap &lt;strong&gt;Test connection&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;strong&gt;A small upstream detour:&lt;/strong&gt; Diction&amp;rsquo;s &lt;strong&gt;My Words&lt;/strong&gt; feature was not reaching the ASR backend during streaming, so I opened &lt;a href="https://github.com/DictionLabs/Diction/pull/20" target="_blank" rel="noopener"&gt;Diction PR #20&lt;/a&gt; to forward those words as the transcription prompt.&#10;With the fix, package names such as &lt;code&gt;PipeFunc&lt;/code&gt; and &lt;code&gt;MindRoom&lt;/code&gt; come back with the correct spelling and capitalization.&#10;It shipped in gateway v13.0, which is why the Compose file needs at least that version.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;p&gt;On my machine, Qwen3-ASR 1.7B uses about 4.7 GiB of GPU memory and roughly 3 GiB of system memory while loaded.&lt;/p&gt;&#10;&lt;h2 id="3-the-bar-test"&gt;3. The bar test &lt;a class="anchor" href="#3-the-bar-test" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;A day later, I met a friend in a bar in the Netherlands and wanted to let him try it.&lt;/p&gt;&#10;&lt;p&gt;He was already on my tailnet for some of my other self-hosted services, so his phone only needed an ACL change to reach the gateway.&#10;I opened the terminal app on my phone, connected to an agent, and dictated that change.&#10;In less than a minute, Diction was connected and working on his phone.&#10;His speech crossed the Atlantic, was transcribed by Qwen on my home GPU, and appeared back in the app with barely any delay.&lt;/p&gt;&#10;&lt;p&gt;That moment sold the whole setup to me.&#10;I was in a bar without a laptop, and I used dictation to grant access to the dictation service.&lt;/p&gt;&#10;</description></item><item><title>Wyoming MLX Whisper</title><link>https://www.nijho.lt/project/wyoming-mlx-whisper/</link><pubDate>Fri, 05 Dec 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/wyoming-mlx-whisper/</guid><description/></item><item><title>Coding from my phone 📱</title><link>https://www.nijho.lt/post/agentic-mobile-workflow/</link><pubDate>Mon, 20 Oct 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/agentic-mobile-workflow/</guid><description>&lt;p&gt;I am dictating this post at ten kilometers altitude on a flight home from Mexico, Blink Shell open on my iPhone, while an agentic assistant on my &lt;a href="https://nixos.org/" target="_blank" rel="noopener"&gt;NixOS&lt;/a&gt; machine writes an initial draft for this post.&#10;Admittedly, I’m a little addicted to agentic coding: when an idea pops up, I want to jump on it right away—even if that means doing it from a cramped seat in the sky.&#10;This is the workflow I’ve arrived at after trying many alternatives, it extends &lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;my agentic coding write-up&lt;/a&gt; and the self-hosted AI obsession in &lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;my local AI journey&lt;/a&gt;.&#10;It&amp;rsquo;s my backup plan for those moments when a computer simply isn&amp;rsquo;t nearby—the rest of the time I&amp;rsquo;m still at a keyboard like any other developer.&#10;This is in no way a replacement for a proper computer/laptop workflow but gets the job done when needed.&#10;It&amp;rsquo;s a personal, mostly open-source stack built around &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt;—my local-first voice layer.&#10;Here&amp;rsquo;s how it works: I speak into my phone, the audio goes to &lt;a href="https://github.com/SYSTRAN/faster-whisper-server" target="_blank" rel="noopener"&gt;FasterWhisper&lt;/a&gt; for transcription, &lt;a href="https://ollama.com/" target="_blank" rel="noopener"&gt;Ollama&lt;/a&gt; cleans up the text, and it lands on my clipboard ready to paste.&#10;My iOS Shortcut handles this through a small HTTP interface that agent-cli exposes.&#10;The coding agent itself still uses the best proprietary model I can access.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;Meta: I&amp;rsquo;m writing about agentic coding on mobile from my actual phone.&#10;It&amp;rsquo;s not the most efficient way to write, but it gets the job done — during a 6.5‑hour flight I dictated and iterated this entire post.&#10;Each iteration was a commit: dictate changes, review the diff, refine, commit, repeat.&#10;See all &lt;a href="https://github.com/basnijholt/nijho.lt/pull/40" target="_blank" rel="noopener"&gt;50+ commits in PR #40&lt;/a&gt; for the full iteration trail.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; iPhone → WireGuard → Blink+Mosh → Zellij.&#10;I dictate via a Shortcut → FasterWhisper (transcribe) → Ollama (polish) → clipboard, then paste into Codex CLI using OpenAI’s &lt;code&gt;gpt-5-codex-high&lt;/code&gt;.&#10;Everything is local except the model.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#1-why-phone-coding-works-now"&gt;1. Why Phone Coding Works Now&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-constraints-and-trade-offs"&gt;2. Constraints and Trade-offs&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-layer-1-wireguard-from-the-router"&gt;3. Layer 1: WireGuard From the Router&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-layer-2-blink-shell--mosh-for-durable-sessions"&gt;4. Layer 2: Blink Shell + Mosh for Durable Sessions&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-layer-3-zellij-layouts"&gt;5. Layer 3: Zellij Layouts&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#6-layer-4-agent-cli-server"&gt;6. Layer 4: &lt;code&gt;agent-cli&lt;/code&gt; Server&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#7-layer-5-the-ios-shortcut-pipeline"&gt;7. Layer 5: The iOS Shortcut Pipeline&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#8-workflow-in-practice"&gt;8. Workflow in Practice&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#9-conclusion-and-further-reading"&gt;9. Conclusion and Further Reading&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="1-why-phone-coding-works-now"&gt;1. Why Phone Coding Works Now &lt;a class="anchor" href="#1-why-phone-coding-works-now" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;For years I used &lt;a href="https://ish.app/" target="_blank" rel="noopener"&gt;&lt;strong&gt;iSH&lt;/strong&gt;&lt;/a&gt; (a full Alpine Linux emulator) with SSH to hop into servers from my phone, but dropped connections were a constant frustration (Mosh fixes this, more on that later).&#10;Because coding on a phone keyboard is terrible, I kept it to tiny configuration tweaks or one-off fixes of a few characters.&#10;Agentic tools changed that: with a CLI coding agent, I don’t need to type the code—I describe the change, review the patch, and run it.&#10;That made meaningful work on the phone possible for the first time, for those moments when a computer isn’t around.&lt;/p&gt;&#10;&lt;p&gt;I tried &lt;strong&gt;VS Code in the browser&lt;/strong&gt;, bounced between &lt;strong&gt;iSH&lt;/strong&gt; and &lt;strong&gt;Terminus&lt;/strong&gt; for SSH, and even lived inside a handful of in-browser terminal clients.&#10;I also spent time with mobile companions like &lt;a href="https://happy.engineering/" target="_blank" rel="noopener"&gt;&lt;strong&gt;Happy&lt;/strong&gt;&lt;/a&gt; and &lt;a href="https://www.ycombinator.com/companies/omnara" target="_blank" rel="noopener"&gt;&lt;strong&gt;Omnara&lt;/strong&gt;&lt;/a&gt;, both designed to mirror Claude Code sessions on the phone, but they still felt like extra steps between me and my actual terminal.&#10;That friction pushed me toward a phone‑ready, self‑hosted workflow that still gives me raw SSH access to my own machine when the computer is out of reach.&lt;/p&gt;&#10;&lt;p&gt;This post is based on the way I develop software today.&#10;Your mileage may vary, but if you also care about privacy, open tooling, and reproducible environments, I think there are useful pieces here.&lt;/p&gt;&#10;&lt;h2 id="2-constraints-and-trade-offs"&gt;2. Constraints and Trade-offs &lt;a class="anchor" href="#2-constraints-and-trade-offs" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This setup grew organically over time. What emerged are less strict requirements and more happy accidents that turned into core benefits:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Single trust boundary:&lt;/strong&gt; Only the model provider (OpenAI) sees code context; audio and automation stay local, and I avoid any additional third‑party relays.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Resilient sessions:&lt;/strong&gt; Connections should survive sleep and spotty networks.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Voice-friendly:&lt;/strong&gt; Dictation should be accurate enough that I can trust it.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Reproducible config:&lt;/strong&gt; The entire stack must live in my &lt;a href="https://www.nijho.lt/post/dotfiles/"&gt;dotfiles&lt;/a&gt; and &lt;a href="https://github.com/basnijholt/dotfiles/tree/8f6bf0b7219195a46a3e010d3538e1e449634db7/configs/nixos" target="_blank" rel="noopener"&gt;NixOS configuration&lt;/a&gt;.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;To give you a sense of what I tried, here is the short comparison that convinced me to roll my own:&lt;/p&gt;&#10;&lt;div class="table-wrap"&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;Approach&lt;/th&gt;&#10;&lt;th&gt;Pros&lt;/th&gt;&#10;&lt;th&gt;Why I moved on&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;VS Code in the browser&lt;/td&gt;&#10;&lt;td&gt;Familiar editor UI&lt;/td&gt;&#10;&lt;td&gt;Needs a steady connection and still lives outside my dotfiles comfort zone&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;iSH / Terminus SSH&lt;/td&gt;&#10;&lt;td&gt;Works without extra infrastructure&lt;/td&gt;&#10;&lt;td&gt;Laggy, no Mosh, and awkward keybindings&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;In-browser terminals&lt;/td&gt;&#10;&lt;td&gt;Instant access from anywhere&lt;/td&gt;&#10;&lt;td&gt;Poor copy/paste ergonomics and flaky mobile keyboards&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://apps.apple.com/us/app/happy-codex-claude-code-app/id6748571505" target="_blank" rel="noopener"&gt;Happy&lt;/a&gt; (Claude Code companion)&lt;/td&gt;&#10;&lt;td&gt;Push notifications, encrypted mobile UI for Claude Code&lt;/td&gt;&#10;&lt;td&gt;Requires wrapping every session with a separate CLI and still abstracts away my shell, and was quite buggy for me&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://omnara.com" target="_blank" rel="noopener"&gt;Omnara&lt;/a&gt; (agent command center)&lt;/td&gt;&#10;&lt;td&gt;Centralizes Claude Code/Codex sessions with terminal replay&lt;/td&gt;&#10;&lt;td&gt;Proxies via their servers; I already trust OpenAI for the model and don&amp;rsquo;t want another third‑party handling my code&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;The stack below gives me the resilience of Mosh, the ergonomics of Zellij, and full control over the AI layer.&lt;/p&gt;&#10;&lt;h2 id="3-layer-1-wireguard-from-the-router"&gt;3. Layer 1: WireGuard From the Router &lt;a class="anchor" href="#3-layer-1-wireguard-from-the-router" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://www.wireguard.com/" target="_blank" rel="noopener"&gt;WireGuard&lt;/a&gt; is a modern VPN that lets me securely access my home network from anywhere.&#10;I terminate WireGuard on my router so every device in the house (and on the road) can dial home with the same config.&#10;(If you want something easier to set up without opening ports, &lt;a href="https://tailscale.com/" target="_blank" rel="noopener"&gt;Tailscale&lt;/a&gt; is a more user-friendly option.)&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Server:&lt;/strong&gt; ASUS XT8 router with WireGuard enabled via the router UI.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Client:&lt;/strong&gt; The WireGuard iOS app with &lt;code&gt;On-Demand&lt;/code&gt; rules so the tunnel flips on whenever I am off trusted Wi-Fi.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;DNS:&lt;/strong&gt; All mobile sessions resolve through my home DNS, so &lt;code&gt;git.nijho.lt&lt;/code&gt; and internal services resolve instantly.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;This gives Blink a local-LAN address for my desktop &lt;code&gt;nixos&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h2 id="4-layer-2-blink-shell--mosh-for-durable-sessions"&gt;4. Layer 2: Blink Shell + Mosh for Durable Sessions &lt;a class="anchor" href="#4-layer-2-blink-shell--mosh-for-durable-sessions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;What is &lt;a href="https://mosh.org/" target="_blank" rel="noopener"&gt;Mosh&lt;/a&gt;?&#10;It&amp;rsquo;s like SSH, but it stays connected when the network changes or the phone sleeps, and it feels faster on bad connections.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://blink.sh/" target="_blank" rel="noopener"&gt;Blink Shell&lt;/a&gt; is my daily driver on iOS because it pairs beautifully with Mosh and has great keyboard ergonomics.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;I launch sessions using &lt;code&gt;mosh bas@nixos -- zellij attach --create phone&lt;/code&gt; (&lt;a href="https://zellij.dev/" target="_blank" rel="noopener"&gt;Zellij&lt;/a&gt; is my terminal multiplexer—more on that in the next section).&lt;/li&gt;&#10;&lt;li&gt;Mosh smooths over spotty LTE and keeps my session alive when the phone sleeps.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Mosh keeps the session responsive across flaky networks and sleep.&lt;/p&gt;&#10;&lt;h2 id="5-layer-3-zellij-layouts"&gt;5. Layer 3: Zellij Layouts &lt;a class="anchor" href="#5-layer-3-zellij-layouts" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I use &lt;strong&gt;Zellij&lt;/strong&gt; as my terminal multiplexer for mobile work.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;I stick to the defaults and use a couple of predefined layouts (e.g., a &amp;ldquo;phone&amp;rdquo; layout with editor + shell panes).&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Ergonomics matter even more on a glass keyboard, so I lean on a few shell helpers:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/ajeetdsouza/zoxide" target="_blank" rel="noopener"&gt;&lt;code&gt;zoxide&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; means I can jump between repos with &lt;code&gt;z foo&lt;/code&gt; instead of pecking long paths.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Single-character aliases&lt;/strong&gt; like &lt;code&gt;p&lt;/code&gt; for &lt;code&gt;pytest&lt;/code&gt; and &lt;code&gt;gs&lt;/code&gt; for &lt;code&gt;git status&lt;/code&gt; live in &lt;a href="https://github.com/basnijholt/dotfiles/blob/8f6bf0b7219195a46a3e010d3538e1e449634db7/configs/shell/10_aliases.sh" target="_blank" rel="noopener"&gt;&lt;code&gt;configs/shell/10_aliases.sh&lt;/code&gt;&lt;/a&gt;, keeping command entry to a minimum.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Autocompletion plugins&lt;/strong&gt;—&lt;a href="https://github.com/zsh-users/zsh-autosuggestions" target="_blank" rel="noopener"&gt;&lt;code&gt;zsh-autosuggestions&lt;/code&gt;&lt;/a&gt; and &lt;code&gt;zsh-syntax-highlighting&lt;/code&gt;—are wired up in &lt;a href="https://github.com/basnijholt/dotfiles/blob/8f6bf0b7219195a46a3e010d3538e1e449634db7/configs/shell/70_zsh_plugins.sh" target="_blank" rel="noopener"&gt;&lt;code&gt;configs/shell/70_zsh_plugins.sh&lt;/code&gt;&lt;/a&gt;, so I get inline hints and colour cues while typing.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I break these tricks down in more depth in &lt;a href="https://www.nijho.lt/post/terminal-ninja/"&gt;Terminal Ninja&lt;/a&gt;, explain how I sync them with my &lt;a href="https://www.nijho.lt/post/dotfiles/"&gt;Dotfiles&lt;/a&gt;, and even package binaries like &lt;code&gt;zoxide&lt;/code&gt; with &lt;a href="https://www.nijho.lt/post/dotbins/"&gt;Dotbins&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="6-layer-4-agent-cli-server"&gt;6. Layer 4: &lt;code&gt;agent-cli&lt;/code&gt; Server &lt;a class="anchor" href="#6-layer-4-agent-cli-server" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt; runs as a small server on my NixOS machine.&#10;In this workflow I only use &lt;code&gt;transcribe&lt;/code&gt; from iOS Shortcuts: the phone records audio, the server transcribes and cleans it up, and I paste the text.&#10;(Agent-CLI has other features like &lt;code&gt;autocorrect&lt;/code&gt; and &lt;code&gt;chat&lt;/code&gt;, but I don&amp;rsquo;t use them for this workflow.)&#10;On this machine I run a long‑lived &lt;code&gt;agent-cli&lt;/code&gt; user service (systemd) so Shortcuts can POST audio to it and get cleaned text back—ready to paste into Codex CLI.&#10;The service is defined in my dotfiles—see: &lt;a href="https://github.com/basnijholt/dotfiles/blob/8f6bf0b7219195a46a3e010d3538e1e449634db7/configs/nixos/modules/user.nix#L29-L40" target="_blank" rel="noopener"&gt;&lt;code&gt;configs/nixos/modules/user.nix&lt;/code&gt; (agent-cli service)&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;For more background on why I built it and how I use it in practice, see &lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;Local AI Journey&lt;/a&gt; (§3) and &lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;Agentic Coding&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;The models and services run on the same box:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;FasterWhisper&lt;/strong&gt; via &lt;a href="https://github.com/SYSTRAN/faster-whisper" target="_blank" rel="noopener"&gt;&lt;code&gt;faster-whisper-server&lt;/code&gt;&lt;/a&gt; for high‑accuracy streaming transcription.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Ollama&lt;/strong&gt; for on‑device rewrite/cleanup before sending prompts to the coding agent.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;My iOS Shortcut dictation workflow is slower than Apple&amp;rsquo;s built-in dictation, but the accuracy is much better—and that matters more.&lt;/p&gt;&#10;&lt;p&gt;For the actual coding agent, I use a &lt;a href="https://github.com/just-every/code" target="_blank" rel="noopener"&gt;fork&lt;/a&gt; of Codex CLI with OpenAI’s &lt;code&gt;gpt-5-codex-high&lt;/code&gt; model (here no open-source solution matches the frontier).&lt;/p&gt;&#10;&lt;h2 id="7-layer-5-the-ios-shortcut-pipeline"&gt;7. Layer 5: The iOS Shortcut Pipeline &lt;a class="anchor" href="#7-layer-5-the-ios-shortcut-pipeline" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My iPhone&amp;rsquo;s Action Button runs a Shortcut that records audio and sends my voice to agent-cli on my NixOS machine for transcription and cleanup.&lt;/p&gt;&#10;&lt;p&gt;For the full recipe, see the iOS Shortcut Guide: &lt;a href="https://github.com/basnijholt/agent-cli/blob/main/iOS_Shortcut_Guide.md" target="_blank" rel="noopener"&gt;agent-cli/iOS_Shortcut_Guide.md&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;In short: I press the Action Button, the Shortcut records a snippet, sends it to &lt;code&gt;agent-cli&lt;/code&gt;, and copies the cleaned text to my clipboard so I can paste it into Codex CLI.&lt;/p&gt;&#10;&lt;p&gt;The whole loop finishes fast enough that I can capture intent by voice and paste it into Codex CLI without typing.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;strong&gt;Dictation quality:&lt;/strong&gt; In my experience, the built‑in iOS dictation is absolute garbage compared to Whisper/FasterWhisper.&#10;Accuracy and punctuation are much better with the FasterWhisper server running at home; it’s a bit slower, but that trade‑off is worth it for clean, usable text.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="8-workflow-in-practice"&gt;8. Workflow in Practice &lt;a class="anchor" href="#8-workflow-in-practice" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Here&amp;rsquo;s the typical flow:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;WireGuard auto-connects when I&amp;rsquo;m off home WiFi&lt;/li&gt;&#10;&lt;li&gt;Open Blink—my Mosh session is still alive from last time, Zellij panes intact&lt;/li&gt;&#10;&lt;li&gt;Dictate the change, paste into Codex CLI, review the agent&amp;rsquo;s response&lt;/li&gt;&#10;&lt;li&gt;Check diffs in an adjacent Zellij pane or different Blink tab with separate Mosh session&lt;/li&gt;&#10;&lt;li&gt;The agent commits and pushes to a branch, then opens a PR via &lt;code&gt;gh&lt;/code&gt; for me to review and merge&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;On the phone, I aim for the smallest practical edits and initial implementations.&#10;Even on personal repos, I still open a PR—sometimes prompting alone gets me to a merge‑ready result.&#10;For open‑source with real users, I finish on the computer with a careful review and any final edits.&lt;/p&gt;&#10;&lt;h2 id="9-conclusion-and-further-reading"&gt;9. Conclusion and Further Reading &lt;a class="anchor" href="#9-conclusion-and-further-reading" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This phone setup extends my &lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;Agentic Coding&lt;/a&gt; workflow to mobile.&#10;I connect to the same Zellij session on &lt;code&gt;nixos&lt;/code&gt;, so when an idea hits, I pick up exactly where I left off—same environment, same context, no setup.&#10;Everything runs on my hardware (voice processing and automation) except the coding model itself.&#10;It’s the most effective mobile workflow I’ve had so far.&#10;It’s mostly open‑source not out of dogma, but because those tools are the best options for my needs.&#10;The one exception is the coding model: there’s no true open equivalent right now, and it makes no sense to buy 20× H100s just to self‑host a frontier model even if such a high quality model would be available open-source.&lt;/p&gt;&#10;&lt;p&gt;If you&amp;rsquo;re curious to dive deeper, here are a few related posts:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;Agentic Coding&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/dotfiles/"&gt;Dotfiles: A Practical, Cross-Platform Terminal Setup&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;Local AI Journey: RTX 3090 Edition&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/terminal-ninja/"&gt;Terminal Ninja&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/using-llms/"&gt;Using LLMs Effectively&lt;/a&gt; (already outdated at this point!)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I&amp;rsquo;d love to hear what self-hosted tricks you&amp;rsquo;re using for mobile development—reach out if you adapt this stack or build something wild on top of it.&lt;/p&gt;&#10;</description></item><item><title>How a 3090 turned into two local‑first AI projects 🎮</title><link>https://www.nijho.lt/post/local-ai-journey/</link><pubDate>Mon, 14 Jul 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/local-ai-journey/</guid><description>&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#1-introduction-the-unplayed-games"&gt;1. Introduction: The Unplayed Games&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-the-rocky-road-to-a-working-setup"&gt;2. The Rocky Road to a Working Setup&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-the-first-victory-agent-cli-"&gt;3. The First Victory: &lt;code&gt;agent-cli&lt;/code&gt; 🐍&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-the-grand-ambition-aibrain-"&gt;4. The Grand Ambition: &lt;code&gt;AIBrain&lt;/code&gt; 🧠&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-conclusion-the-journey-continues"&gt;5. Conclusion: The Journey Continues&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="1-introduction-the-unplayed-games"&gt;1. Introduction: The Unplayed Games &lt;a class="anchor" href="#1-introduction-the-unplayed-games" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I&amp;rsquo;ve wanted to dabble with local AI for a long time, mainly because of the possibilities it unlocks.&#10;Local AI allows me to use all of my data—financial records, personal messages, location history, even transcribing everything I say—without the privacy concerns of sending it to a cloud provider.&#10;This level of deep, personal data analysis is only something I&amp;rsquo;d be comfortable with on my own hardware.&lt;/p&gt;&#10;&lt;p&gt;However, I couldn&amp;rsquo;t justify buying a powerful GPU just for an imagined AI project that I might never really start.&#10;Then it hit me: I could also use it for gaming (duh).&#10;Because I really enjoyed &lt;em&gt;The Last of Us&lt;/em&gt; series, I thought, “&lt;em&gt;Okay, why not play &lt;strong&gt;The Last of Us&lt;/strong&gt; on PC?&lt;/em&gt;”&#10;This was the push I needed.&#10;So, a couple of weeks ago, I convinced myself to buy someone&amp;rsquo;s old gaming machine.&#10;After many deep research sessions with ChatGPT about the best bang-for-the-buck consumer-grade GPU, I landed on an NVIDIA 3090.&#10;I found an (old) beast of a machine, and my initial idea was set.&lt;/p&gt;&#10;&lt;details&gt;&#10; &lt;summary&gt;Click here to see the full system specs ($1350 for all this!)&lt;/summary&gt;&#10; &lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;GPU:&lt;/strong&gt; ASUS ROG STRIX RTX 3090 OC (24GB)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;CPU:&lt;/strong&gt; AMD Ryzen 9 3900X (12-Core)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Motherboard:&lt;/strong&gt; ASUS ROG Crosshair VIII Hero&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Power Supply:&lt;/strong&gt; Be Quiet! 1200W Dark Power Pro&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;RAM:&lt;/strong&gt; 32GB G.Skill Trident Z Royal DDR4&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Storage:&lt;/strong&gt; 2TB Intel M.2 NVMe SSD&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Case:&lt;/strong&gt; Corsair 4000D Airflow&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;CPU Cooler:&lt;/strong&gt; Thermalright Peerless Assassin 120&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&#10;&lt;/details&gt;&#10;&#10;&lt;p&gt;Fast forward four weeks.&#10;I haven&amp;rsquo;t played a single minute, but my machine is now indexing 200 GB of email archives and facing a serious performance bottleneck I&amp;rsquo;ll get to later.&#10;That &amp;ldquo;gaming&amp;rdquo; machine has been humming away, but not rendering virtual worlds.&#10;It&amp;rsquo;s been training, transcribing, and thinking.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;m writing this post simply because I am so enthusiastic about local AI.&#10;I&amp;rsquo;ve found that local models are surprisingly powerful for specific use cases.&#10;Of course, they aren&amp;rsquo;t going to replace Gemini or Claude for coding, but for many other applications, they work incredibly well.&#10;Lately, I&amp;rsquo;ve been so obsessed with this that I&amp;rsquo;m sleeping significantly less and spending all my free hours working on it.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; This post is the story of how a quest for gaming turned into a deep dive into local, private AI.&#10;I&amp;rsquo;ll share the two open-source Python packages that came out of it, &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/basnijholt/aibrain" target="_blank" rel="noopener"&gt;&lt;code&gt;AIBrain&lt;/code&gt;&lt;/a&gt;, and the lessons I learned along the way.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="2-the-rocky-road-to-a-working-setup"&gt;2. The Rocky Road to a Working Setup &lt;a class="anchor" href="#2-the-rocky-road-to-a-working-setup" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Before I could even get to the fun AI stuff, I had to wrestle with the machine itself.&#10;I started with Pop!_OS, which is supposed to be a great out of the box solution that &amp;ldquo;just works&amp;rdquo;.&#10;However, it&amp;rsquo;s been a long time since I used Linux on the desktop; my experience is almost exclusively with servers.&#10;I quickly installed some of the wrong NVIDIA drivers and ended up debugging stuff in Grub.&#10;I got frustrated with the different commands I had to run to set up everything and realized that reproducing this system was going to be a massive pain.&#10;Since I&amp;rsquo;ve grown to love Nix on my Mac with Nix-Darwin, I decided to switch the whole system to &lt;strong&gt;NixOS&lt;/strong&gt;.&#10;This has been an amazing decision.&#10;Even though there are many pain points with Nix, I think it is well worth it.&#10;For those interested, you can see the complete setup, including all the local AI services, in my &lt;a href="https://github.com/basnijholt/dotfiles/blob/841cd5fb94b65556e7a2cc7d8ea618f86b50bf1d/configs/nixos/configuration.nix" target="_blank" rel="noopener"&gt;NixOS configuration on GitHub&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="3-the-first-victory-agent-cli-"&gt;3. The First Victory: &lt;code&gt;agent-cli&lt;/code&gt; 🐍 &lt;a class="anchor" href="#3-the-first-victory-agent-cli-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;With a stable system, I quickly got started with a few scripts.&#10;These soon evolved into my first real project of this adventure: &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;agent-cli&lt;/code&gt; is a collection of &lt;strong&gt;local-first&lt;/strong&gt;, AI-powered command-line agents that run entirely on your machine.&#10;The core philosophy is privacy; your data never leaves your computer.&#10;It&amp;rsquo;s designed for seamless integration with system-wide hotkeys.&#10;I&amp;rsquo;m now using it constantly.&lt;/p&gt;&#10;&lt;p&gt;Instead of typing, I now dictate almost everything.&#10;A quick hotkey, and &lt;code&gt;agent-cli transcribe&lt;/code&gt; turns my speech into text.&#10;Another hotkey, and &lt;code&gt;agent-cli autocorrect&lt;/code&gt; cleans it up using a local LLM running on &lt;strong&gt;Ollama&lt;/strong&gt;.&#10;It has genuinely streamlined my workflow.&#10;For example, I can pipe the tools together, like correcting text from my clipboard and then having it spoken aloud: &lt;code&gt;agent-cli autocorrect | agent-cli speak&lt;/code&gt;.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;p&gt;&lt;strong&gt;⚡ Surprising Speed: Local vs. Cloud&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;One of my biggest surprises was the raw speed of local models.&#10;While I added an OpenAI provider to &lt;code&gt;agent-cli&lt;/code&gt; for those without local hardware (someone I know wanted to try it), I consistently find that local execution is faster than waiting on a cloud API.&#10;For example, a local transcription with Whisper + Ollama lands in my clipboard in well under a second—often beating the network round-trip to a remote service.&lt;/p&gt;&#10;&lt;/div&gt;&lt;/aside&gt;&#10;&lt;p&gt;The toolkit includes:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;autocorrect&lt;/code&gt;: Fixes grammar and spelling.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;transcribe&lt;/code&gt;: Uses a local Whisper model for speech-to-text.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;speak&lt;/code&gt;: Converts text to speech with a local TTS engine.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;voice-edit&lt;/code&gt;: A voice-powered clipboard assistant.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;assistant&lt;/code&gt;: A hands-free voice assistant using a wake word.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;chat&lt;/code&gt;: A conversational AI with tool-calling capabilities.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;It&amp;rsquo;s been a fantastic success in my personal workflow, and it&amp;rsquo;s all open-source for you to try.&#10;Contributions and ideas welcome!&lt;/p&gt;&#10;&lt;p&gt;Once I trusted my toolchain, the obvious next step was to feed &lt;em&gt;all&lt;/em&gt; my data into it.&lt;/p&gt;&#10;&lt;h2 id="4-the-grand-ambition-aibrain-"&gt;4. The Grand Ambition: &lt;code&gt;AIBrain&lt;/code&gt; 🧠 &lt;a class="anchor" href="#4-the-grand-ambition-aibrain-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My second package, &lt;a href="https://github.com/basnijholt/aibrain" target="_blank" rel="noopener"&gt;&lt;code&gt;AIBrain&lt;/code&gt;&lt;/a&gt;, is a much larger endeavor.&#10;The vision is to create a &amp;ldquo;life-OS&amp;rdquo;—a private, local AI that processes all my personal data (emails, calendar, files, photos, messages, browser history, location data) to generate summaries and answer questions, eventually allowing for queries like:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;aibrain ask &lt;span class="s2"&gt;&amp;#34;When did I get into landscape photography?&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;aibrain ask &lt;span class="s2"&gt;&amp;#34;How many countries did I visit between 2015-2025?&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This project led me to explore the landscape of agentic frameworks.&#10;I started with &lt;strong&gt;CrewAI&lt;/strong&gt;, but I quickly found that despite the hype and funding, it had some painful problems (specifically &lt;a href="https://github.com/crewAIInc/crewAI/issues/3031" target="_blank" rel="noopener"&gt;this issue&lt;/a&gt; where things just fail without any helpful error messages).&#10;I decided to switch to &lt;strong&gt;LangGraph&lt;/strong&gt;, which felt more robust.&#10;I also chose to not use &lt;strong&gt;PydanticAI&lt;/strong&gt; (which I used in &lt;code&gt;agent-cli&lt;/code&gt;) because its &lt;strong&gt;Ollama&lt;/strong&gt; support is just a wrapper around the OpenAI-compatible API, which doesn&amp;rsquo;t expose all the useful options (I even opened &lt;a href="https://github.com/ollama/ollama/pull/11249/" target="_blank" rel="noopener"&gt;a PR&lt;/a&gt; in Ollama to make its OpenAI compatible API more feature-complete, but it has not been merged yet).&lt;/p&gt;&#10;&lt;p&gt;The core infrastructure of &lt;code&gt;AIBrain&lt;/code&gt; is ready.&#10;It can index emails, calendar events, and files in various formats (PDF, Excel, PowerPoint, images).&#10;However, I&amp;rsquo;ve hit a significant challenge.&lt;/p&gt;&#10;&lt;aside class="callout callout-warning"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M236.8,188.09,149.35,36.22h0a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.35,24.35,0,0,0,40.55,224h174.9a24.35,24.35,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM222.93,203.8a8.5,8.5,0,0,1-7.48,4.2H40.55a8.5,8.5,0,0,1-7.48-4.2,7.59,7.59,0,0,1,0-7.72L120.52,44.21a8.75,8.75,0,0,1,15,0l87.45,151.87A7.59,7.59,0,0,1,222.93,203.8ZM120,144V104a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,180Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;p&gt;&lt;strong&gt;🔥 The Performance Bottleneck&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;Even with a 12-core CPU and 24GB GPU, processing just a few days’ worth of email made the fans scream for half an hour.&#10;Processing hundreds of gigabytes of personal data is a massive undertaking, and it&amp;rsquo;s clear that my current approach isn&amp;rsquo;t scalable enough.&lt;/p&gt;&#10;&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="5-conclusion-the-journey-continues"&gt;5. Conclusion: The Journey Continues &lt;a class="anchor" href="#5-conclusion-the-journey-continues" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This past month has been a whirlwind.&#10;I set out to play games and ended up with two open-source AI packages and a much deeper appreciation for the power sitting in a consumer-grade GPU.&#10;It&amp;rsquo;s incredible what you can achieve with local hardware today.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;agent-cli&lt;/code&gt; is a finished, polished tool that I use daily.&#10;&lt;code&gt;AIBrain&lt;/code&gt; is a much bigger dream, and I&amp;rsquo;m still figuring out how to solve the performance puzzle to make it truly practical.&#10;Next up: smarter chunking with LangGraph, vector-DB swap-outs, and GPU scheduling tricks.&lt;/p&gt;&#10;&lt;p&gt;This experience has reinforced my passion for local-first software and open-source development.&#10;The future of personal, private AI is being built right now, not just in large corporate labs, but in the homes of hobbyists tinkering with their gaming rigs.&lt;/p&gt;&#10;&lt;p&gt;⭐ If this resonates, star the repos &amp;amp; drop issues/questions.&lt;/p&gt;&#10;&lt;p&gt;And who knows, maybe I&amp;rsquo;ll even find the time to play &amp;lsquo;The Last of Us&amp;rsquo;.&lt;/p&gt;&#10;</description></item><item><title>Agent CLI</title><link>https://www.nijho.lt/project/agent-cli/</link><pubDate>Wed, 25 Jun 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/agent-cli/</guid><description/></item></channel></rss>