<?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: Abhash Chakraborty</title>
    <description>The latest articles on DEV Community by Abhash Chakraborty (@abhash_chakraborty).</description>
    <link>https://dev.to/abhash_chakraborty</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%2F2818938%2F42ffd28f-8e25-4530-b294-e4254d69ea16.png</url>
      <title>DEV Community: Abhash Chakraborty</title>
      <link>https://dev.to/abhash_chakraborty</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abhash_chakraborty"/>
    <language>en</language>
    <item>
      <title>I built a remote for the coding agents in my tmux</title>
      <dc:creator>Abhash Chakraborty</dc:creator>
      <pubDate>Thu, 15 Oct 2026 19:08:11 +0000</pubDate>
      <link>https://dev.to/abhash_chakraborty/i-built-a-remote-for-the-coding-agents-in-my-tmux-1o4b</link>
      <guid>https://dev.to/abhash_chakraborty/i-built-a-remote-for-the-coding-agents-in-my-tmux-1o4b</guid>
      <description>&lt;p&gt;Inyon lets me answer the coding agents running on my machines from my phone. It's free, open source, and as of this week anyone can install it with one line or from npm.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Walking back to press Enter
&lt;/h2&gt;

&lt;p&gt;I run Claude Code and Codex on a laptop and two servers, and each one is signed in to a different account. A long task usually stops at some point to ask something small: can I run the tests, can I edit this file, which of these two approaches. Until I answered, nothing moved.&lt;/p&gt;

&lt;p&gt;The agents have their own remote features, but each follows one account at a time, and some only work if you started the agent their way. My agents were already running in tmux, often started over SSH an hour earlier. I didn't want to restart them inside something else. I wanted to answer them where they were.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Inyon does
&lt;/h2&gt;

&lt;p&gt;Inyon reads every agent session in tmux across my machines and shows each one as a chat: replies, tool calls, diffs, plans and to-dos. When an agent asks a question, the options arrive as buttons. I tap one, and Inyon types it into the pane, exactly as if I'd pressed the keys.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy32t2fv4k1foojlwn7um.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy32t2fv4k1foojlwn7um.webp" alt="A recording of the desktop overview: a Codex session asks to run bun test, the Yes button is pressed, and the session goes back to work" width="600" height="398"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Codex asks before it runs the tests. The answer goes from the overview straight into its tmux pane.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Nothing changes at the keyboard. When I sit back down, the terminal is where I left it, and the agent never knew anything was different. Each machine keeps its own account, because Inyon never signs in to anything.&lt;/p&gt;

&lt;p&gt;It works with Claude Code, Codex, Gemini CLI, OpenCode, GitHub Copilot CLI and Antigravity CLI, on Linux, macOS and Windows.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reading an agent without wrapping it
&lt;/h2&gt;

&lt;p&gt;Most remotes for agents start the agent themselves, so they see everything it does. I chose to read from outside instead, which is harder and more fragile, but it means any pane works, including one started before Inyon was installed.&lt;/p&gt;

&lt;p&gt;Claude Code and Codex write their conversations to session files, which give the chat. Questions are harder: agents don't publish them anywhere. Each one draws its prompt on the screen in its own layout, and that layout changes between versions. So Inyon reads the pane's screen with a parser per agent, tested against prompts captured from real sessions. That's the honest weak spot: a new agent release can break a parser until I update it, and three of the six readers are still in beta.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F42xpdy8zyq6v2lboorex.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F42xpdy8zyq6v2lboorex.webp" alt="A Codex prompt in a tmux pane with three numbered answers is captured by the hub, turned into a Needs-you card with Yes, Always and No buttons, and the tap goes back to the pane as tmux send-keys" width="799" height="453"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The question on the pane's screen becomes buttons, and the tap is typed back into the pane&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Reaching a laptop with no server
&lt;/h2&gt;

&lt;p&gt;I didn't want to run a server for anyone, and a laptop usually sits behind a router with no open ports.&lt;/p&gt;

&lt;p&gt;When a computer is set up for the phone, &lt;code&gt;inyon&lt;/code&gt; opens a free Cloudflare quick tunnel. It signs the tunnel's current address with its own key and publishes it with pkarr, so the phone can always find it. The phone checks that signature and runs a Noise handshake with the computer's key, which it got from the QR code, and the computer only lets in phones it has paired. The tunnel only ever carries ciphertext. If the machines share a VPN like Tailscale or WireGuard, the Android app tries that address alongside the tunnel and keeps whichever answers first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi7xiyswgryzyfwl4szq4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi7xiyswgryzyfwl4szq4.webp" alt="The computer running inyon publishes a signed pkarr record of its address, the phone checks it after scanning the QR code, connects through a Cloudflare quick tunnel, and the two finish a Noise IK handshake with X25519 and ChaCha20-Poly1305" width="799" height="453"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Pairing: a signed address, a free tunnel, and a handshake the tunnel can only carry, never read&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The cost of having no server is notifications. Only a push service can wake a closed Android app, and I won't run one, so the app keeps a small "Inyon is connected" notification and gets no alerts once it's closed. In a browser, each computer sends standard Web Push itself.&lt;/p&gt;
&lt;h2&gt;
  
  
  Making it safe to install
&lt;/h2&gt;

&lt;p&gt;Inyon types into the same panes as my agents, so I'm asking people to trust it with a lot. I spent more time than I expected on the install path.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every release's checksum list is signed with minisign. &lt;code&gt;inyon setup&lt;/code&gt; and &lt;code&gt;inyon update&lt;/code&gt; refuse a list without a good signature before they download anything, and the one-line installer checks it too when minisign is installed.&lt;/li&gt;
&lt;li&gt;The website's download mirror checks the same signature and every file, and publishes nothing if one fails.&lt;/li&gt;
&lt;li&gt;On npm, the packages are published only by the release workflow, through npm's trusted publishing. There's no npm token to steal, each version carries a provenance statement, and nothing runs at install time.&lt;/li&gt;
&lt;li&gt;Every third-party GitHub Action in the release pipeline is pinned to an exact commit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this makes Inyon bug-free. It does mean the file you install is the one the release built.&lt;/p&gt;
&lt;h2&gt;
  
  
  What's rough
&lt;/h2&gt;

&lt;p&gt;There's no iOS app yet. The Gemini, OpenCode and Copilot readers are in beta. On Windows, through psmux, the terminal view shows what's on screen without scrollback.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;On each computer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://inyon.abhashchakraborty.tech/get.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or &lt;code&gt;npm install -g @abhash-chakraborty/inyon&lt;/code&gt;, then &lt;code&gt;inyon setup&lt;/code&gt;. Scan the QR code with the Android app from the &lt;a href="https://inyon.abhashchakraborty.tech/download/" rel="noopener noreferrer"&gt;download page&lt;/a&gt;, or run a hub and use a browser. The &lt;a href="https://inyon.abhashchakraborty.tech/docs/" rel="noopener noreferrer"&gt;docs&lt;/a&gt; cover the rest, and the code is on &lt;a href="https://github.com/Abhash-Chakraborty/inyon" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://abhashchakraborty.tech/projects/inyon?utm_source=devto" rel="noopener noreferrer"&gt;project page&lt;/a&gt; has more on how I built it. Next I want an iOS app and fewer beta labels, and I'd like to hear which agents you'd use it with.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://abhashchakraborty.tech/writing/inyon?utm_source=devto" rel="noopener noreferrer"&gt;abhashchakraborty.tech&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>tmux</category>
      <category>ai</category>
      <category>android</category>
    </item>
    <item>
      <title>Parakeet Redux: is a 178 MB speech model useful in the real world?</title>
      <dc:creator>Abhash Chakraborty</dc:creator>
      <pubDate>Tue, 06 Oct 2026 19:08:12 +0000</pubDate>
      <link>https://dev.to/abhash_chakraborty/parakeet-redux-is-a-178-mb-speech-model-useful-in-the-real-world-1fjk</link>
      <guid>https://dev.to/abhash_chakraborty/parakeet-redux-is-a-178-mb-speech-model-useful-in-the-real-world-1fjk</guid>
      <description>&lt;p&gt;Moondream recently released &lt;a href="https://huggingface.co/moondream/parakeet-redux" rel="noopener noreferrer"&gt;Parakeet Redux&lt;/a&gt;, a speech-to-text model that fits in 178 MB. It's NVIDIA's &lt;a href="https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3" rel="noopener noreferrer"&gt;Parakeet TDT 0.6B v3&lt;/a&gt;, one of the strongest open speech models, with its weights cut down to three values each. The original is 1,256 MB, and the model card says accuracy stays close to it.&lt;/p&gt;

&lt;p&gt;A 0.6B-parameter model in roughly the space of a 61M one would change where good speech recognition can run: on an ordinary CPU, offline, inside an app you download in seconds. So I wanted to know what it actually gives up. I tested it against its own original, a standard 8-bit copy of that original, four other open models and three cloud APIs. First on my laptop, then again on a cloud machine with an NVIDIA Tesla T4, to see how it does on a GPU.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The short version: Redux is 7× smaller than its original and was faster on every device I tried: 15× real time on six laptop CPU cores and 85× on the T4, about 15% ahead of the original there. In exchange it made 0.6 to 0.8 more errors per 100 words, mostly on accented speech and dictation. And the small file doesn't mean a small footprint: it used about 1 GB of RAM on a CPU and 2 GB of GPU memory on the T4. English only, measured on 5 and 6 October 2026.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Parakeet Redux is
&lt;/h2&gt;

&lt;p&gt;Redux doesn't change the model's design. It keeps the architecture of Parakeet TDT 0.6B v3, and its files name that model as the "teacher" it was trained against. What changes is how the weights are stored.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhm8rq06h34ptbebmw11y.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhm8rq06h34ptbebmw11y.webp" alt="Diagram of Parakeet Redux: 16 kHz audio becomes 128 mel bands, a 16-bit subsampling stage makes one frame every 80 ms, a 24-layer Conformer encoder with ternary weights (604 million weights, 121 MB) does the listening, and a 16-bit TDT decoder emits tokens and durations. A small voice activity head marks speech every 80 ms. Of the 178 MB, 121 MB is the ternary encoder and 55 MB stays 16-bit" width="800" height="507"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Read from the model's own config.json, ternary.json and weights file.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The input:&lt;/strong&gt; 16 kHz audio turned into 128 mel bands, then shortened eight times by a subsampling stage, so the model sees one frame for every 80 ms of sound.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The encoder&lt;/strong&gt; does the listening: 24 Conformer layers, each with self-attention (8 heads), a convolution (kernel 9) and two feed-forward blocks, 1,024 wide. This is where almost all of the weights live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The decoder&lt;/strong&gt; is a TDT (token-and-duration transducer): a small prediction network (a two-layer LSTM, 640 wide) and a joint network that emit a token together with how many frames to skip. Skipping ahead is a big part of why Parakeet models are fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A voice activity head:&lt;/strong&gt; a tiny 0.2M-parameter side head that marks speech and silence every 80 ms, so the model can tell where the talking is on its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it got so small
&lt;/h2&gt;

&lt;p&gt;Normally each weight is a 16-bit number. Redux turns the encoder's 604 million weights into ternary values: every weight is −1, 0 or +1, and each group of 128 weights shares one scale. Three possible values need about 1.58 bits, and Redux packs five weights into a single byte (35 = 243 combinations, which fits in 256), so each weight costs 1.6 bits instead of 16. In the released file, 44% of those weights are exactly zero.&lt;/p&gt;

&lt;p&gt;That shrinks the encoder to 121 MB. The parts that are small but sensitive stay in 16-bit: the decoder, the joint network, the token embeddings, the subsampling layers and the per-group scales, about 55 MB together. Add a couple of MB of normalization values and you get the 178 MB on disk.&lt;/p&gt;

&lt;p&gt;Ternary weights also make the math simpler: multiplying by −1, 0 or +1 is just adding, subtracting or skipping. Moondream's Photon engine is built around that, with ternary matrix kernels for CPUs with AVX2, AVX-512 or AVX-VNNI, and a CUDA path documented for NVIDIA Ampere or newer GPUs. The model is released under CC-BY-4.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I tested it
&lt;/h2&gt;

&lt;p&gt;I wrote the method down before collecting a single result and logged every decision after that. The other nine systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parakeet TDT 0.6B v3 at 16-bit (1,256 MB) and standard 8-bit (740 MB): Redux's original.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://huggingface.co/nvidia/parakeet-unified-en-0.6b" rel="noopener noreferrer"&gt;Parakeet Unified EN 0.6B&lt;/a&gt;, an English-only model that can stream (731 MB, 8-bit).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://huggingface.co/openai/whisper-large-v3" rel="noopener noreferrer"&gt;Whisper large-v3&lt;/a&gt; (1.55B parameters, 1,669 MB) and &lt;a href="https://huggingface.co/openai/whisper-large-v3-turbo" rel="noopener noreferrer"&gt;Whisper large-v3-turbo&lt;/a&gt; (0.8B, 886 MB).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://huggingface.co/UsefulSensors/moonshine-base" rel="noopener noreferrer"&gt;Moonshine base&lt;/a&gt;, a 61M-parameter model built for edge devices (132 MB).&lt;/li&gt;
&lt;li&gt;Three cloud APIs: OpenAI &lt;code&gt;gpt-transcribe&lt;/code&gt;, ElevenLabs &lt;code&gt;scribe_v2&lt;/code&gt; and Google's &lt;code&gt;gemini-3.5-transcribe&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;About 35 minutes of English audio, cut into 147 pieces, each with a reference transcript:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real-world:&lt;/strong&gt; six YouTube clips with human captions: a TED talk, a podcast, an MIT lecture, a news segment, a TEDx talk by an Indian-English speaker, and street interviews.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Benchmark sets:&lt;/strong&gt; 50 utterances from LibriSpeech, Earnings-22, AMI meetings and VoxPopuli, from the &lt;a href="https://huggingface.co/datasets/hf-audio/esb-datasets-test-only-sorted" rel="noopener noreferrer"&gt;Open ASR Leaderboard test sets&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accents:&lt;/strong&gt; 12 speakers with 12 different first languages reading the same passage, from the &lt;a href="https://accent.gmu.edu/" rel="noopener noreferrer"&gt;Speech Accent Archive&lt;/a&gt; (CC BY-NC-SA 2.0).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Noise:&lt;/strong&gt; ten of the clean clips again, with pink noise at 5 dB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dictation:&lt;/strong&gt; me reading four paragraphs (225 words) into the laptop microphone. I didn't read the script word for word, so its reference is the script corrected to what I actually said, using where the models agreed. That can favour the models, so treat dictation numbers as a hint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Accuracy is word error rate (WER) after the Whisper text normalizer, on the 78 public clips (5,531 words), with 95% confidence intervals from 10,000 bootstrap resamples. 5% WER means about five wrong words in every 100. Speed comes from 20 short benchmark clips (3 minutes of audio): a warm-up, then the median of three timed passes, one clip at a time.&lt;/p&gt;

&lt;p&gt;There were two setups, with the same audio, references and scoring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;My laptop:&lt;/strong&gt; an AMD Ryzen 5 5600H (6 cores, 6 threads used), 15.3 GB of RAM and an NVIDIA GTX 1650 4 GB, on Windows 11. The other open models ran in transcribe-cpp with &lt;a href="https://huggingface.co/handy-computer" rel="noopener noreferrer"&gt;community GGUF conversions&lt;/a&gt;, on the GPU through Vulkan. Redux ran in Photon on the CPU only: its GPU path isn't documented for a card this old, so I didn't use it there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A cloud T4:&lt;/strong&gt; a Tesla T4 16 GB with a 2-core Xeon (4 threads) and 32 GB of RAM, on Ubuntu. Here each model ran in the engine people usually deploy it with: Photon for Redux, NVIDIA NeMo for the original Parakeets, faster-whisper for Whisper and Hugging Face transformers for Moonshine. The cloud APIs weren't run again.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Accuracy
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbdhe0i74bvrnvo9f8l3k.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbdhe0i74bvrnvo9f8l3k.webp" alt="Bar chart of word error rate with 95% confidence intervals from the laptop run, Parakeet Redux highlighted: ElevenLabs 4.03%, OpenAI 4.09%, Parakeet Unified 4.16%, Whisper turbo 4.34%, Gemini 4.38%, Parakeet TDT 8-bit 4.52%, Parakeet TDT 16-bit 4.54%, Whisper large-v3 5.04%, Parakeet Redux 5.33%, Moonshine 7.02%" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Laptop run: 78 public clips, 5,531 words. Redux in green.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On the laptop Redux scored 5.33%, behind its original (4.54%) and Whisper large-v3 (5.04%), and well ahead of Moonshine (7.02%). The top of the table is a five-way tie: ElevenLabs (4.03%), OpenAI (4.09%), Parakeet Unified (4.16%), Whisper turbo (4.34%) and Gemini (4.38%), with confidence intervals that overlap almost completely. With 78 clips, gaps under about a point aren't reliable.&lt;/p&gt;

&lt;p&gt;On the T4 the ranking of the open models held: Parakeet Unified 4.25%, Whisper turbo 4.56%, the original Parakeet 4.72%, Whisper large-v3 4.88%, Redux 5.32% on the GPU and 5.50% on the CPU, Moonshine 7.07%.&lt;/p&gt;

&lt;p&gt;One result about compression on its own: the 8-bit original scored 4.52% against 4.54% for the 16-bit one. In this sample, going from 16-bit to 8-bit cost nothing measurable. Going from 8-bit to ternary is where a cost shows up.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4pr4nuhweq00nppcu6ln.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4pr4nuhweq00nppcu6ln.webp" alt="Scatter plot of word error rate against weights on disk, Parakeet Redux highlighted at 178 MB and 5.33%, next to Moonshine at 132 MB and 7.02%, with the larger Parakeet and Whisper models between 731 MB and 1,669 MB at 4.16% to 5.04%" width="800" height="508"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Weights on disk against WER. The grey band is the three cloud APIs, whose sizes aren't published.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Plotted against size, Redux is the odd one out: everything in its accuracy range is four to nine times bigger. Size in general predicts little here; the 1.67 GB Whisper large-v3 scored worse than three models half its size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redux against its own original
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffj27b9ezwpoh5vdwworb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffj27b9ezwpoh5vdwworb.webp" alt="Parakeet Redux against its original: weights 178 MB against 740 MB at 8-bit and 1,256 MB at 16-bit; extra word error rate on the laptop and the T4: clean benchmark clips +0.20 and +0.41, real-world YouTube +0.78 and +0.38, noise +1.09 and +1.09, accents +1.45 and +1.57, dictation +1.78 and +2.67; speed 15.1 against 10.7 times real time on the laptop CPU, 7.7 against 6.6 and 4.8 on the Xeon CPU, 85.0 against 73.8 on the T4; 1.0 to 1.3 GB of RAM on a CPU and 2.0 GB of GPU memory on the T4" width="800" height="440"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Same audio, same architecture. On the laptop the original ran as GGUF; on the T4, in NeMo.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the comparison the whole test was built around. On identical clips Redux made &lt;strong&gt;+0.80&lt;/strong&gt; more errors per 100 words than its original on the laptop (95% interval −0.08 to +1.54) and &lt;strong&gt;+0.60&lt;/strong&gt; on the T4 (−0.25 to +1.26). The two setups agree on the direction and roughly the size. But both intervals include zero, and both reach past the +0.5 points I'd set in advance as "no meaningful loss", so 78 clips can't say whether the true gap is tiny or about a point and a half.&lt;/p&gt;

&lt;p&gt;Where the gap lands is clearer than its size. On clean benchmark clips it was small, +0.2 on the laptop and +0.4 on the T4. On the 12 accented speakers it was about +1.5 on both, and on my dictation +1.8 and +2.7. Its mistakes were phonetic: "flight lines" for "lands", "revenue grow" for "grew". The model card reports a 6.55% average on seven Open ASR Leaderboard sets and no accent breakdown; on that kind of audio the gap here was small too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two setups
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff2ssyqvbod6th3tisbeu.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff2ssyqvbod6th3tisbeu.webp" alt="Table of the two machines and Parakeet Redux on each: laptop CPU 5.33% word error rate, 15.1 times real time, 0.48 seconds per clip, 982 MB of RAM; Xeon CPU 5.50%, 7.7 times, 0.97 seconds, 1,349 MB; T4 GPU 5.32%, 85.0 times, 0.09 seconds, 2,093 MB of GPU memory; its original on the T4 4.72%, 73.8 times, 0.11 seconds, 2,875 MB of GPU memory. Identical transcripts: Redux laptop CPU against Xeon CPU 134 of 147, Redux Xeon CPU against T4 GPU 127 of 147, Parakeet Unified laptop CPU against Xeon CPU 144 of 147" width="799" height="493"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The T4 numbers come from two runs on the same machine type.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It runs on the T4.&lt;/strong&gt; Photon documents its CUDA path for Ampere or newer, and the T4 is an older Turing card. It worked anyway, and fast. That's one card, though, not a promise for every Turing GPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The server CPU was slower than my laptop.&lt;/strong&gt; Redux ran at 7.7× on the 2-core Xeon against 15.1× on six laptop cores. The Xeon has AVX-512 but not VNNI, so Photon used the same AVX2 kernel as on the laptop, and its fastest CPU path is still untested here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rankings shift with the machine.&lt;/strong&gt; On the laptop the 8-bit original was faster than the 16-bit one; on the Xeon it was the other way round (4.8× against 6.6×). The machines differ in cores, clocks, engines and operating system, so I can't say which of those caused it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The text changes a little too.&lt;/strong&gt; Redux's transcripts were word-for-word identical on 134 of 147 pieces between the two CPUs, and on 127 of 147 between the Xeon's CPU and the T4. Accuracy stayed about the same, but don't expect the exact same output on every device.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it holds up, and where it slips
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvyv7846vtdz2h3oxuqyr.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvyv7846vtdz2h3oxuqyr.webp" alt="Heat table of word error rate for every system on every kind of audio from the laptop run, with Parakeet Redux's row outlined in green: low error on clean audiobooks, earnings calls and noise, higher on accents, dictation and the street clip" width="800" height="508"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Laptop run. Each column has 147 to 715 words, so read one column as a hint.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strong:&lt;/strong&gt; earnings calls (5.5%, level with the best system), the podcast (3.0%), clean audiobooks (1.1%), meetings (8.2%) and noise (1.8%).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weak:&lt;/strong&gt; accented speakers (3.6% against 2.2% for its original on the laptop), my dictation, and the street interviews (11.9%, among the worst).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No system wins everywhere.&lt;/strong&gt; ElevenLabs led on meetings, Whisper turbo on the lecture, Whisper large-v3 on accents and dictation, and Gemini in noise. Whisper large-v3 also lost points on the podcast for a reason that isn't really a mistake: it drops repeated fillers like "you know", which the captions keep.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of the ten systems wrote the name "Ananya" in my dictation as "Aranya". I can't tell whether that's the models or my pronunciation, so it stays in the reference and counts as an error for all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Speed
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjoesm691jn2qak4emvib.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjoesm691jn2qak4emvib.webp" alt="Real-time factor on a Tesla T4, the laptop's GTX 1650 and the laptop CPU, on a log scale, Parakeet Redux highlighted: 85.0 times real time on the T4, not tested on the GTX 1650, 15.1 on the laptop CPU; Parakeet TDT 73.8, 38.7 and 7.6; Parakeet Unified 46.1, 38.5 and 9.3; Moonshine 24.3, 65.7 and 20.3; Whisper turbo 25.5, 6.2 and 0.8; Whisper large-v3 12.8, 4.8 and 0.6" width="800" height="573"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Seconds of audio per second of compute; median of 3 timed passes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On the laptop CPU Redux ran at 15.1× real time, about four minutes for an hour of audio. That's 1.4× the 8-bit original in the same Linux container (10.7×) and twice the 16-bit one (7.6×), which makes it the fastest Parakeet on that CPU. It wasn't the fastest model overall: Moonshine ran at 20.3×, with clearly more errors. Whisper was slower than real time on this CPU.&lt;/p&gt;

&lt;p&gt;On the T4 Redux was the fastest system I tested, at 85.0× against 73.8× for its original in NeMo and 46.1× for Parakeet Unified. At that rate an hour of audio would take about 42 seconds. That's worked out from short clips run one at a time, not a timed hour-long job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Latency and memory
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4xx87zcqli8x2oxm94py.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4xx87zcqli8x2oxm94py.webp" alt="Latency per clip, median and 95th percentile, with memory, sorted fastest first: Parakeet Redux on the T4 0.09 seconds with 2,093 MB of GPU memory; Parakeet models on the T4 and GTX 1650 0.11 to 0.21 seconds; Parakeet Redux on the laptop CPU 0.48 seconds with 982 MB of RAM; cloud APIs 1.03 to 3.03 seconds; Whisper large-v3 on the laptop CPU 13.75 seconds" width="800" height="567"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;One 4 to 25 second clip at a time with the model loaded. Cloud times include the upload from my connection.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For dictation, the wait per sentence matters most. Redux took a median 0.09 s on the T4 and 0.48 s on the laptop CPU, quicker than every cloud API from my connection (1.03 s for ElevenLabs, 1.05 s for OpenAI, 3.03 s for Gemini). Whisper always processes a 30-second window, so even a short sentence took 11 to 14 seconds on the laptop CPU.&lt;/p&gt;

&lt;p&gt;Memory is where the 178 MB figure misleads. Redux used about 980 MB of RAM on the laptop and 1.35 GB on the Xeon, not far from the 8-bit original. On the T4 it took about 2 GB of GPU memory, less than its original's 2.9 GB but nothing like the file size. The small download is real; a tiny runtime isn't, at least not with today's engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs in real use
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7339od5ouxb773a4ee70.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7339od5ouxb773a4ee70.webp" alt="Table of what each option costs in practice: Parakeet Redux makes 5.3 wrong words per 100, answers in 0.48 seconds on the laptop CPU or 0.09 seconds on a T4, has no per-hour price and transcribes 1,000 hours in 66 hours on the laptop CPU or 12 hours on a T4, keeping the audio on your own machine; cloud APIs cost $40 to about $68 for a year of daily 30-minute dictation and $220 to about $370 for 1,000 hours, and the audio goes to the provider" width="800" height="527"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Two everyday jobs, with cloud list prices on 5 October 2026.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two everyday jobs make the trade clearer. Dictating half an hour a day for a year is about 180 hours of speech: $40 with ElevenLabs, $49 with OpenAI and roughly $68 with Gemini at their list prices on 5 October, or no per-hour price with a local model. Transcribing an archive of 1,000 hours costs $220 to $370 in the cloud. With Redux the same archive would take about 66 hours on my laptop's CPU, or about 12 hours on a T4. Check current prices before you rely on these; they change.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developers.openai.com/api/docs/models/gpt-transcribe" rel="noopener noreferrer"&gt;OpenAI gpt-transcribe&lt;/a&gt;: $0.27 an hour. Per &lt;a href="https://developers.openai.com/api/docs/guides/your-data" rel="noopener noreferrer"&gt;OpenAI's data controls&lt;/a&gt;, API audio isn't used for training by default.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://elevenlabs.io/pricing/api" rel="noopener noreferrer"&gt;ElevenLabs Scribe v2&lt;/a&gt;: $0.22 an hour. Audio is retained by default; &lt;a href="https://elevenlabs.io/docs/eleven-api/resources/zero-retention-mode" rel="noopener noreferrer"&gt;zero retention&lt;/a&gt; is for enterprise plans.&lt;/li&gt;
&lt;li&gt;Gemini 3.5 Transcribe: billed per token rather than per minute, which works out to roughly $0.37 an hour by my estimate.&lt;/li&gt;
&lt;li&gt;Local models: no per-hour price, and the audio stays on your own machine. That isn't the same as free; I didn't measure hardware, server time or electricity.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Every model at a glance
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpcgprjydxa86cmvsrzf2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpcgprjydxa86cmvsrzf2.webp" alt="Table of all ten systems with size, word error rate overall, on accents and on dictation, speed on the T4, the laptop GPU and the laptop CPU, wait per sentence, price per hour, and what each is best for and gives up; Parakeet Redux is highlighted: 178 MB, 5.33% WER, 85 times real time on the T4 and 15 times on the laptop CPU, 0.48 seconds per sentence, best for small downloads, CPU transcription and offline apps, weaker on accents and dictation and using 1 to 2 GB of memory" width="800" height="579"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Accuracy from the laptop run; speed in multiples of real time; the wait is the median on the laptop.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When it's worth using
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;English transcription on a CPU, on Linux.&lt;/strong&gt; This is where it's strongest: the fastest Parakeet on both CPUs I tested, from a 178 MB download. Plan for about 1 to 1.4 GB of RAM and a small accuracy cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apps that ship or download the model.&lt;/strong&gt; 178 MB against 740 MB for the 8-bit original and 1,256 MB for the 16-bit one. I didn't measure the size of the engine itself, which you'd ship too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU batch jobs.&lt;/strong&gt; On the T4 it was the fastest system here, with somewhat lower accuracy than its original. It worked on this card despite not being documented for it; test yours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean audio in bulk.&lt;/strong&gt; The gap on benchmark-style clips was small. I didn't test many requests at once, so measure throughput under your own load.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where to be careful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Accented speakers, names and dictation.&lt;/strong&gt; That's where the extra errors were. Test on the people who'll actually use it; 12 accented speakers and one dictating voice is a small sample.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows.&lt;/strong&gt; On my AVX2 laptop the Windows build of Photon had no AVX2 kernel for this CPU, so Redux wouldn't run natively; it ran fine in a Linux container on the same machine. That's one version on one CPU, and may well change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small-memory devices.&lt;/strong&gt; Phones and boards with little RAM weren't tested, and the 1 GB-plus runtime is the thing to check first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If every word matters&lt;/strong&gt; and you have the memory, the 8-bit original was more accurate, showed no measurable loss against 16-bit here, and runs in more places today.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ternary weights work.&lt;/strong&gt; A 0.6B speech model in 178 MB, faster than its original on every device I tried, with a cost you mostly won't notice on clean audio. That's a real step for on-device speech.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Averages hide where compression hurts.&lt;/strong&gt; The loss was small on benchmark-style audio and larger on accents and dictation, on both setups. If you ship a model like this, test it on your own speakers, not on leaderboard averages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A small file isn't a small runtime.&lt;/strong&gt; 178 MB of weights still needed a gigabyte of RAM on a CPU and two of GPU memory. Download size and memory are different budgets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The engine matters as much as the weights.&lt;/strong&gt; Most of Redux's practical limits were packaging, not the model: no native Windows kernel for my CPU, GPU support documented only for newer cards, and a runtime that uses most of a gigabyte. The T4 result shows the documented limits aren't the whole story either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For English, local models have caught up with the cloud.&lt;/strong&gt; On the laptop the best open models tied three commercial APIs on accuracy, and Redux sits within about a point of them while running offline with no per-hour price. The cloud still offers languages, speaker labels and other features I didn't test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small samples.&lt;/strong&gt; 78 public clips, 12 accented speakers who read the same passage, and 225 dictated words from one speaker. The ten noisy clips are copies of clean ones, which the intervals don't account for, so treat them as rough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;English only.&lt;/strong&gt; The original Parakeet TDT v3 is multilingual; I didn't test other languages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engines are mixed in.&lt;/strong&gt; Redux in Photon against its original in NeMo or GGUF compares two deployments, not compression alone. Start-up times weren't measured under equal conditions, so I haven't compared them, and memory figures are sampled while running, not exact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardware I didn't have.&lt;/strong&gt; No CPU with VNNI, no Ampere-or-newer GPU, no Apple Silicon or ARM board.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud results are from 5 October 2026&lt;/strong&gt;, from one network, with that day's models and prices.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd test next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A CPU with VNNI and a GPU Photon officially supports, recording which kernel and precision it actually uses.&lt;/li&gt;
&lt;li&gt;Apple Silicon and an ARM board, where small models matter most.&lt;/li&gt;
&lt;li&gt;Accents at scale, 100+ speakers, and the other languages Parakeet supports.&lt;/li&gt;
&lt;li&gt;Native Windows on an AVX2-only CPU, once a new Photon build is out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks to Moondream for Parakeet Redux, NVIDIA for Parakeet, OpenAI for Whisper, Useful Sensors for Moonshine, the Open ASR Leaderboard maintainers for the test sets, and Steven Weinberger and George Mason University for the Speech Accent Archive.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://abhashchakraborty.tech/writing/how-small-can-speech-recognition-get?utm_source=devto" rel="noopener noreferrer"&gt;abhashchakraborty.tech&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>speechrecognition</category>
      <category>opensource</category>
      <category>benchmark</category>
    </item>
    <item>
      <title>Fine-tuning a 0.8B model to beat hosted LLMs at one job</title>
      <dc:creator>Abhash Chakraborty</dc:creator>
      <pubDate>Mon, 05 Oct 2026 16:07:46 +0000</pubDate>
      <link>https://dev.to/abhash_chakraborty/fine-tuning-a-08b-model-to-beat-hosted-llms-at-one-job-4ajl</link>
      <guid>https://dev.to/abhash_chakraborty/fine-tuning-a-08b-model-to-beat-hosted-llms-at-one-job-4ajl</guid>
      <description>&lt;p&gt;I needed a model for one narrow decision. When a new fact about someone comes in, does it replace something already known? "I moved to Berlin" should retire "I live in Pune". "Watched two episodes of The Bear" should leave "works at Infosys" alone.&lt;/p&gt;

&lt;p&gt;It sounds trivial, but any assistant with long-term memory makes this call all the time, and both kinds of mistake hurt. Miss an update and it keeps repeating stale facts. Replace too eagerly and it throws away things that are still true.&lt;/p&gt;

&lt;p&gt;This post covers the models I compared, how I fine-tuned the best starting point, how I tested it, and what it took to train. The short version: a 0.8B open model, fine-tuned on 6,000 examples on a single T4, beat every hosted LLM I tested at this one job.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9kfyd0j285qzii50727h.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9kfyd0j285qzii50727h.webp" alt="Bar chart: the fine-tuned kev-mem scores 0.954 mean ROC AUC on memory-update decisions, ahead of Gemini 3.7 Flash (0.892), Gemma 4 31B (0.887), GPT-5.4 mini (0.873), Kev as released (0.852), Jev (0.834) and Laya (0.711)" width="799" height="453"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Every model on the same held-out tests. For my definition of an update, not a general ranking.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a System-One model
&lt;/h2&gt;

&lt;p&gt;The obvious approach is to prompt an LLM and parse what comes back. That works, but it's slow, every decision is an API call, and the answer is free text that you then have to interpret.&lt;/p&gt;

&lt;p&gt;TypeSafe's Jev takes a different approach. It's a "System One" model: instead of writing text, it answers typed questions with calibrated probabilities, like a choice from a list or a yes/no. For this task I send it the new fact and up to eight existing ones, and ask two questions about each existing fact:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How does the new fact relate to it: duplicate, updates, elaborates, related or unrelated?&lt;/li&gt;
&lt;li&gt;Is the existing fact now outdated?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The score I use is the average of the two: &lt;code&gt;p_update = (P(updates) + P(outdated)) / 2&lt;/code&gt;. At 0.70 or above, the old fact is replaced.&lt;/p&gt;

&lt;h2&gt;
  
  
  The candidates
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Jev&lt;/strong&gt; (hosted, by TypeSafe). Solid, but tuned for general decisions rather than my definition of an update.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Laya&lt;/strong&gt;, an open 421M ModernBERT model that answers in Jev's format. Fast and light, but off the shelf it replaced too many everyday events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kev&lt;/strong&gt;, Jared Palmer's open-weight model, compatible with Jev. I tried two sizes. Kev-0.8B was the best of everything as released on stated updates. Kev-4B scored lower on that, and on the T4 it ran out of memory on longer requests and took 5.5 seconds per decision.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For reference I also ran a zero-shot NLI model (DeBERTa-v3-large) and several hosted LLMs, each prompted with the same definition of an update. Kev-0.8B was the clear starting point: the best as released, and the only size that both trains and runs on a 16 GB T4.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data
&lt;/h2&gt;

&lt;p&gt;Each training example is one request: a new fact, up to eight existing facts, and the typed questions, with a label for every question. I built them from two pools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;6,818 logged Jev decisions.&lt;/strong&gt; Training on these is distillation: Kev learns to make Jev's calls on real requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10,219 decisions with ground truth.&lt;/strong&gt; I simulated 48 lives of a year each. People move city, change jobs, partners and managers, switch gyms, cars, courses, diets and laptops, reschedule appointments, mention crucial facts and make small talk. Because the simulator knows what actually changed, every pair has the right answer: same attribute with a new value is an update, the same value is a duplicate, anything else is related or unrelated. That's 53,870 fact pairs, 2,298 of them real updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 6,000 training examples took every decision that contains a real update first, then filled up from the shuffled mix. Nothing from the test data went in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 1: supervised fine-tuning
&lt;/h2&gt;

&lt;p&gt;LoRA on Qwen3.5-0.8B-Base, starting from Kev's released weights: rank 16, alpha 32, dropout 0.05, on every linear and linear-attention projection, plus a small decision head. One epoch over the 6,000 examples at a learning rate of 5e-5, with label smoothing of 0.05.&lt;/p&gt;

&lt;p&gt;The GPU was a single NVIDIA T4 with 16 GB. It has no bf16, so everything ran in fp32, with batch 1, gradient accumulation of 8 and gradient checkpointing. The requests are short (87 tokens on average, 138 at the 95th percentile), so nothing was truncated. That came to 750 optimizer steps and 5.52M tokens in 6.55 hours, peaking at 5.09 GB of GPU memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2: reward-ranked RL (ReST)
&lt;/h2&gt;

&lt;p&gt;Supervised training teaches the format and the definition, but it doesn't know which mistakes matter more. So I let the stage-1 model make the real decision, replace or keep at the 0.70 threshold, on 2,000 training decisions (10,574 pairs), and scored every pair:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;+1 for catching a real update&lt;/li&gt;
&lt;li&gt;−1 for missing one&lt;/li&gt;
&lt;li&gt;−2 for replacing a fact that was still true&lt;/li&gt;
&lt;li&gt;+0.25 for correctly keeping a fact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mean reward was 0.265. There were 15 wrong replacements and 65 missed updates, spread over 52 decisions with a negative reward. Those 52 were relabelled with the truth (repeated twice when they included a −2) and mixed with replayed stage-1 examples, for 366 records in total. A second, gentler pass at a learning rate of 1e-5 took 46 steps, 0.34M tokens and 23 minutes, peaking at 4.98 GB.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxihm3elhjp9fjabmun45.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxihm3elhjp9fjabmun45.webp" alt="The training run on one NVIDIA T4: stage 1, supervised fine-tuning on 6,000 decisions, 750 steps, 5.52M tokens, 6.55 hours, 5.09 GB peak; stage 2, ReST on 366 records, 46 steps, 0.34M tokens, 23 minutes, 4.98 GB peak; 885M parameters of which 11.3M trained" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Both stages on one T4. About 12 GPU-hours for the whole project, baseline tests included.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I picked the final checkpoint on six separate validation lives (400 decisions), where it scored 0.9991 AUC against 0.9983 for stage 1. The test sets were never used to choose anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I tested it
&lt;/h2&gt;

&lt;p&gt;Two held-out tests, both kept out of training:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A two-year simulated life&lt;/strong&gt; with template text: 122 decisions, 428 pairs, 186 updates, 45 of them stated outright.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A live run&lt;/strong&gt; where an LLM wrote the facts from a three-month simulated life, so they read more like real conversation: 127 decisions, 498 pairs, 104 updates, 36 stated. This is the harder and more realistic one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A stated update says it directly ("I moved to Berlin"). An implied one doesn't, like a new address in another city with no mention of moving. I report both.&lt;/p&gt;

&lt;p&gt;The main metric is ROC AUC. Take one real update and one non-update at random: AUC is the chance the model scores the real update higher. 0.5 is a coin flip and 1.0 is perfect. It isn't accuracy, so 0.954 doesn't mean "right 95% of the time". I also check precision and recall at the 0.70 threshold, because that's where the model actually acts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fva096zy9wjkhmvttxn96.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fva096zy9wjkhmvttxn96.webp" alt="Line chart: on the live test, Kev's ROC AUC for stated updates goes from 0.752 as released to 0.929 after SFT and 0.945 after ReST, above Jev's 0.781; for any update it goes from 0.708 to 0.873 and 0.872" width="800" height="467"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The live test. Most of the gain comes from SFT; ReST makes the model more careful about replacing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On the live test, stated updates went from 0.752 as released to 0.929 after stage 1 and 0.945 after stage 2. For any update, including implied ones, it went from 0.708 to 0.873 and 0.872. Jev scores 0.781 and 0.570 on the same test. On the two-year life the stated score went from 0.952 to 0.962, and for any update from 0.583 to 0.840.&lt;/p&gt;

&lt;p&gt;At the 0.70 threshold on the live test, precision was 1.00: it never replaced a fact that was still true. The cost is a recall of 0.14, so it only replaces when it's very sure. Kev and Laya as released never crossed the threshold at all. For this job, a cautious model is the right default, and a lower threshold chosen on validation data recovers more of the misses.&lt;/p&gt;

&lt;p&gt;Averaged over both tests on stated updates, with every hosted LLM given the same definition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;my fine-tuned Kev (kev-mem): &lt;strong&gt;0.954&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Gemini 3.7 Flash: 0.892&lt;/li&gt;
&lt;li&gt;Gemma 4 31B: 0.887&lt;/li&gt;
&lt;li&gt;GPT-5.4 mini: 0.873&lt;/li&gt;
&lt;li&gt;Kev as released: 0.852&lt;/li&gt;
&lt;li&gt;Jev: 0.834&lt;/li&gt;
&lt;li&gt;DeBERTa-v3-large NLI, zero-shot: 0.815&lt;/li&gt;
&lt;li&gt;Laya: 0.711&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Size and cost
&lt;/h2&gt;

&lt;p&gt;The model has about 885M parameters: 873.4M in the base, 10.8M in the LoRA adapter and 0.5M in the decision head, so only 11.3M were trained. The adapter is 43 MB. It runs in about 4 GB of GPU memory and takes 0.81 seconds per decision on the T4, without any of the optimised kernels. Counting the baseline tests and a second stage-1 run, the whole project used about 12 GPU-hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;This is my use case and my definition of an update. Jev and Kev are general-purpose models and the hosted LLMs were only prompted, so a lot of the gain comes from specialising. That's the point, but it isn't a general ranking.&lt;/li&gt;
&lt;li&gt;These are single training runs, with no confidence intervals yet.&lt;/li&gt;
&lt;li&gt;Jev answered 234 of the 249 test decisions; the rest failed after retries.&lt;/li&gt;
&lt;li&gt;The test lives are simulated, apart from the LLM-written text of the live run.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://huggingface.co/abhash-chakraborty/kev-mem-0.8b" rel="noopener noreferrer"&gt;kev-mem-0.8b on Hugging Face&lt;/a&gt;, served through Kev's own server, so any Jev client works with it&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.kaggle.com/models/abhashchakraborty/kev-mem" rel="noopener noreferrer"&gt;kev-mem on Kaggle&lt;/a&gt; and the &lt;a href="https://www.kaggle.com/code/abhashchakraborty/kev-mem-system-1-benchmark" rel="noopener noreferrer"&gt;benchmark notebook&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://www.kaggle.com/benchmarks/tasks/abhashchakraborty/memory-update-detection" rel="noopener noreferrer"&gt;memory-update detection benchmark&lt;/a&gt; for hosted LLMs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks to TypeSafe for Jev, Jared Palmer for Kev, the Laya authors for open-sourcing their work (Apache-2.0), and the Qwen team for Qwen3.5.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://abhashchakraborty.tech/writing/fine-tuning-a-small-model-for-memory-updates?utm_source=devto" rel="noopener noreferrer"&gt;abhashchakraborty.tech&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>llm</category>
      <category>finetuning</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building a portfolio that feels fast</title>
      <dc:creator>Abhash Chakraborty</dc:creator>
      <pubDate>Mon, 05 Oct 2026 12:05:04 +0000</pubDate>
      <link>https://dev.to/abhash_chakraborty/building-a-portfolio-that-feels-fast-29o1</link>
      <guid>https://dev.to/abhash_chakraborty/building-a-portfolio-that-feels-fast-29o1</guid>
      <description>&lt;p&gt;I started rebuilding my portfolio this summer with a short brief: it should feel like mine, it should open fast, and I should be able to change any of it without a deploy. The first two pull against each other. The third turns what could have been a static site into one with a backend.&lt;/p&gt;

&lt;p&gt;This is a write-up of how the pieces fit together, the problems that took longest, and what I settled on. Most of it applies to any content site that wants to be quick without being plain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the system
&lt;/h2&gt;

&lt;p&gt;Almost every request is someone reading, so I optimised for reads and accepted a bit more work on writes. Pages are React Server Components rendered on Vercel and cached as static HTML. A visitor's browser never talks to the database. It gets HTML, a few small interactive islands, and images from a separate storage domain.&lt;/p&gt;

&lt;p&gt;Content lives in Convex: my profile, projects, posts, likes and the game leaderboard. Images and files live in Cloudflare R2 on their own domain. A private editor, which I call the Studio, writes to both. Product analytics end up in PostHog, but only after passing through the site's own pipeline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwvsymq9h9fgl2di3ff04.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwvsymq9h9fgl2di3ff04.webp" alt="System overview: the visitor, Next.js on Vercel with static pages and client islands, Convex, PostHog, Cloudflare R2 and the Studio" width="800" height="507"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Reads come from cached HTML, writes come from the Studio, and events pass through an outbox before they leave.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A rich design on a light page
&lt;/h2&gt;

&lt;p&gt;The design leans on texture: brushed silver plates, a monochrome portrait that picks up colour on hover, and a holographic glint on project cards. Each of those could easily have cost a library and a few hundred kilobytes.&lt;/p&gt;

&lt;p&gt;The rule I followed was simple. If something can be HTML and CSS, it is. JavaScript runs only in small client components that need state: the header, the command palette, the live status strip and the terminal. Each game is its own chunk and loads when you start it. Everything else ships as markup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzhphln1xatidhjekqa6j.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzhphln1xatidhjekqa6j.webp" alt="Design system board: silver, ink and green colour tokens, the rainbow holo foil palette, the Geist type scale, components and corner radii" width="800" height="560"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The tokens behind the look: eight colours, the holo foil that only appears on hover, one type family and three radii.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The rainbow foil shows how to keep an effect cheap. It's a conic gradient seen through a striped mask, and hovering only moves one pre-drawn layer with a transform, so nothing repaints while the pointer moves. On touch screens, where there's no hover, the same layers play as a slow pulse.&lt;/p&gt;

&lt;p&gt;Scrolling was the subtle part. A few sections animate as they scroll into view, which means the browser marks styles dirty on every frame. Early on, the scroll rail read element positions inside its scroll handler, which forced a full layout each frame and made the top of the page stutter. The fix was to measure once on load and on resize, cache the numbers, and only ever animate opacity and transforms so the compositor does the work.&lt;/p&gt;

&lt;p&gt;Order matters as much as size. The hero portrait and the status request are preloaded so they download alongside the HTML, and anything optional, like analytics, waits until the browser is idle. On a cold load from India, the first byte arrives in 83 ms and the largest paint lands at about 0.3 seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzygd7h54qvu9w1n4t8ra.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzygd7h54qvu9w1n4t8ra.webp" alt="Request waterfall for one cold load of the homepage, with first paint at 276 ms and largest paint at 304 ms" width="799" height="457"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;One cold load in Edge with an empty cache, in milliseconds from the start of navigation.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Editing without redeploying
&lt;/h2&gt;

&lt;p&gt;I wanted to write and fix things from a browser, but I didn't want page views to depend on a database call. The answer is a cache that the editor is allowed to break.&lt;/p&gt;

&lt;p&gt;All public reads go through one small content layer. Each query is cached under a shared tag and revalidated hourly. When I save something, the save clears that tag, so the next request renders fresh HTML while every other request keeps coming from cache. If the database is unreachable, the profile and projects fall back to copies that ship with the code, so the site stays up even when the backend doesn't.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwniotd0c3hjpyd5v1ut6.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwniotd0c3hjpyd5v1ut6.webp" alt="Publishing flow in four steps: the editor, a server action that checks the session and sanitizes HTML, a Convex transaction, and the site clearing its content cache" width="799" height="333"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;What one save does: sanitize once, write once, and invalidate only the cached content that changed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Posts and project write-ups use a Tiptap editor. It stores two things: the editor's structured document, which is what I edit, and HTML, which is what the site renders. The HTML is sanitized on the server against an allowlist on every save, so the site can render it directly without trusting the editor.&lt;/p&gt;

&lt;p&gt;Charts, metrics, flow diagrams and galleries were harder. They're React components, and I didn't want stored HTML to be able to render arbitrary components. In storage each one is an empty placeholder carrying its type and a small JSON payload. When a page renders, the HTML is split at those placeholders, the payload is validated, and the real component goes in its place. A malformed block renders nothing instead of breaking the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Previews that never leak
&lt;/h2&gt;

&lt;p&gt;The Studio shows the real page next to the editor, not a lookalike. That uses the framework's draft mode, which skips the cache and lets pages read unpublished content. The risk is obvious: draft mode is carried by the browser, and anything a browser carries can be copied.&lt;/p&gt;

&lt;p&gt;So draft mode alone isn't enough. A page only reads drafts when the request also carries a valid editor session, and the public queries never return unpublished posts at all. Previews are also scoped to the tab that asked for one, so opening the site in another tab after an editing session shows exactly what visitors see.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fybr04g4bjjv9usu6udca.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fybr04g4bjjv9usu6udca.webp" alt="The Studio's Site editor with the homepage sections on the left and the live page on the right" width="800" height="507"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Site editor: each section's controls on the left, the real page in draft mode on the right.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A terminal that owns the keyboard
&lt;/h2&gt;

&lt;p&gt;The site has a terminal. Pages behave like folders, so &lt;code&gt;cd projects&lt;/code&gt; takes you there, and it has six games: Snake, Space Defender, Type Racer, 2048, Minesweeper and a daily 2048 challenge.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftiw8re99ljobi5czwac9.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftiw8re99ljobi5czwac9.webp" alt="The terminal's game picker showing six games" width="800" height="507"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The game picker. Each card acts out its game on hover.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The hardest bug came from a browser extension. Some copy-paste "unlocker" extensions stop every key press before the page sees it. In Edge that meant Space scrolled the page and the games ignored the arrow keys. Listening on the window in the capture phase fixed it, because those listeners run before anything attached to the document. Focus needed the same care: games take focus without scrolling, and the terminal never calls &lt;code&gt;scrollIntoView&lt;/code&gt;, which moves the page as well as the container.&lt;/p&gt;

&lt;p&gt;Scores taught me about leaving. A run used to count only when it ended, so closing the tab mid-game threw it away. Now each game saves its score so far when you exit, switch tabs or close the window. Posting lives in a module that outlives the terminal, keeps a verification token ready while you play, and sends the last score with a keepalive request so it survives the page closing. The server still checks every score against the game's rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analytics without tracking people
&lt;/h2&gt;

&lt;p&gt;I want to know which pages get read and where people give up, not who they are. That rules out most off-the-shelf setups, so the site runs its own small pipeline and only forwards anonymised events.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5bbxb0uob0o6ut8ndnyp.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5bbxb0uob0o6ut8ndnyp.webp" alt="Analytics pipeline: the browser, a same-origin endpoint that hashes the visitor, a Convex transaction, and delivery to PostHog" width="799" height="313"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;From a page view to a chart. Nothing that identifies a person is stored along the way.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The browser sends a page view once the page is idle, then the time the page was actually on screen and how far down it was read. Read depth comes from four invisible markers watched by an IntersectionObserver, so nothing measures the layout while you scroll. On the way out, a beacon carries the last update, so closing the tab doesn't lose it.&lt;/p&gt;

&lt;p&gt;On the server a visitor is a one-way hash. By default it's built from values that change every day, so it can count visits without following anyone from one day to the next, and the inputs themselves are never stored. Visitors who choose to be remembered get a random id instead, which is what makes returning-visitor numbers possible. Each event is written to the database together with the counters it affects, in one transaction. A scheduled job then delivers it to PostHog and backs off and retries if PostHog is having a bad day.&lt;/p&gt;

&lt;p&gt;Counting correctly took more thought than collecting. A reload shouldn't add a reader, so each post keeps one row per reader per day, and later updates only add the difference. The number shown under a post counts each reader once. Shares count when someone actually opens the shared link rather than when a button is pressed, because copying a link isn't sharing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd keep
&lt;/h2&gt;

&lt;p&gt;Most of the speed came from deciding what not to do on the hot path: no database on reads, no layout work while scrolling, nothing optional before the first paint. Most of the reliability came from writing things down before acting on them: content into a cache that's cleared on purpose, events into an outbox before they leave.&lt;/p&gt;

&lt;p&gt;If I started again I'd keep the same order: settle a few visual rules, make reading fast, then add the fun parts where they don't get in the way of either.&lt;/p&gt;

&lt;p&gt;You can browse the &lt;a href="https://abhashchakraborty.tech/projects?utm_source=devto" rel="noopener noreferrer"&gt;projects&lt;/a&gt; or open the &lt;a href="https://abhashchakraborty.tech/terminal?utm_source=devto" rel="noopener noreferrer"&gt;terminal&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://abhashchakraborty.tech/writing/building-a-portfolio-that-feels-fast?utm_source=devto" rel="noopener noreferrer"&gt;abhashchakraborty.tech&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>performance</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
