<?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: ThatToolSite</title>
    <description>The latest articles on DEV Community by ThatToolSite (@thattoolsite).</description>
    <link>https://dev.to/thattoolsite</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%2F4176101%2F940957a8-0a97-42b6-a4c0-f19c8e50428b.png</url>
      <title>DEV Community: ThatToolSite</title>
      <link>https://dev.to/thattoolsite</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thattoolsite"/>
    <language>en</language>
    <item>
      <title>How to Fix Common JSON Syntax Errors (Incl. AI Output)</title>
      <dc:creator>ThatToolSite</dc:creator>
      <pubDate>Tue, 13 Oct 2026 16:00:06 +0000</pubDate>
      <link>https://dev.to/thattoolsite/how-to-fix-common-json-syntax-errors-incl-ai-output-16gb</link>
      <guid>https://dev.to/thattoolsite/how-to-fix-common-json-syntax-errors-incl-ai-output-16gb</guid>
      <description>&lt;p&gt;The &lt;a href="https://thattoolsite.com/json-validator" rel="noopener noreferrer"&gt;JSON validator&lt;/a&gt; shows the exact line and column of the first error, explains it in plain words, and can repair the usual mistakes. Your JSON never leaves the browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://thattoolsite.com/json-validator" rel="noopener noreferrer"&gt;Check your JSON&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the error message, then look one line up
&lt;/h2&gt;

&lt;p&gt;A JSON parser reads from the top and stops at the first character that can't be right. The line and column it reports are where it stopped, which is often just after the real mistake. In this example the comma is missing at the end of line 3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Report"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The validator reports &lt;strong&gt;line 4, column 3: Missing comma between two properties&lt;/strong&gt;, because it only knows something is wrong when it meets &lt;code&gt;"pages"&lt;/code&gt;. So when the reported line looks fine, check the end of the line before it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistakes behind most invalid JSON
&lt;/h2&gt;

&lt;p&gt;JSON is stricter than the JavaScript or Python it resembles. RFC 8259, the JSON standard, allows only double-quoted strings, lowercase &lt;code&gt;true&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt; and &lt;code&gt;null&lt;/code&gt;, and commas only between items.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mistake&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trailing comma&lt;/td&gt;
&lt;td&gt;["a", "b",]&lt;/td&gt;
&lt;td&gt;Delete the comma before ] or }&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single quotes&lt;/td&gt;
&lt;td&gt;{'name': 'Ada'}&lt;/td&gt;
&lt;td&gt;Use double quotes for keys and strings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key without quotes&lt;/td&gt;
&lt;td&gt;{name: "Ada"}&lt;/td&gt;
&lt;td&gt;Quote every key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comments&lt;/td&gt;
&lt;td&gt;// settings&lt;/td&gt;
&lt;td&gt;Remove them; JSON has no comments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python values&lt;/td&gt;
&lt;td&gt;True, None&lt;/td&gt;
&lt;td&gt;true, null&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Curly quotes&lt;/td&gt;
&lt;td&gt;“hello”&lt;/td&gt;
&lt;td&gt;Straight double quotes, " … "&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Numbers JSON can't read&lt;/td&gt;
&lt;td&gt;.5, 007, NaN&lt;/td&gt;
&lt;td&gt;0.5, "007" (as text, to keep the zeros), null&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing comma&lt;/td&gt;
&lt;td&gt;"a": 1 "b": 2&lt;/td&gt;
&lt;td&gt;Add a comma between items&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Fixing JSON from ChatGPT, Claude or Gemini
&lt;/h2&gt;

&lt;p&gt;JSON copied from an AI answer has its own typical problems, on top of the ones above. The answer wraps the JSON in a Markdown code fence and a sentence, and models trained on JavaScript slip in trailing commas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;Here&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;JSON:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;```json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ada"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"math"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;,],&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;```&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A strict parser stops at the very first character, the H of “Here”. Try to fix in the validator removes the sentence and the fence and deletes the two trailing commas, leaving valid JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ada"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"math"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The other common AI problem is a reply that stops mid-way because it reached its length limit. The JSON then ends inside a string or without its closing brackets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check what an automatic repair changed
&lt;/h2&gt;

&lt;p&gt;Repairs are guesses, and most are safe: removing a comment or a trailing comma can't change your data. Two kinds need a human look. Take this cut-off reply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Lamp"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cab
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try to fix closes the string and the brackets, so the result is valid, and lists both changes. But the second item now has the name &lt;code&gt;"Cab"&lt;/code&gt;, and whatever came after it is gone. Valid is not the same as complete. Ask the assistant to continue, or to send the JSON again in smaller parts.&lt;/p&gt;

&lt;p&gt;The second kind is a missing comma or quote in the middle of a document, where the repair has to decide where one value ends and the next begins. Read the list of changes, and compare the item count with what you expected. If you need to see exactly what moved, put the original and the repaired version side by side in &lt;a href="https://thattoolsite.com/json-diff" rel="noopener noreferrer"&gt;JSON diff&lt;/a&gt; once both parse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Valid JSON that a program still rejects
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Duplicate keys.&lt;/strong&gt; The standard says names should be unique but doesn't forbid repeats. JavaScript and Python keep the last value without warning. The validator flags them.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;A byte order mark.&lt;/strong&gt; The standard says JSON sent between systems must not start with one, and Python's &lt;code&gt;json.loads&lt;/code&gt; refuses a string that starts with one. Save the file as UTF-8 without a BOM.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Big numbers.&lt;/strong&gt; IDs above 9,007,199,254,740,991 lose their last digits in JavaScript. Send them as strings.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Lenient parsers.&lt;/strong&gt; Python accepts &lt;code&gt;NaN&lt;/code&gt; and &lt;code&gt;Infinity&lt;/code&gt; by default, so a file can work in one program and fail in another. Check against the standard, not one parser.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once it's valid, the &lt;a href="https://thattoolsite.com/json-formatter" rel="noopener noreferrer"&gt;JSON formatter&lt;/a&gt; lays it out with indentation so it's easy to read and review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://www.rfc-editor.org/rfc/rfc8259" rel="noopener noreferrer"&gt;RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://docs.python.org/3/library/json.html" rel="noopener noreferrer"&gt;Python documentation: json&lt;/a&gt;. NaN, duplicate keys and the BOM&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thattoolsite.com/guides/fix-json-syntax-errors" rel="noopener noreferrer"&gt;ThatToolSite&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>json</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Count Your Tokens Before You Paste: Practical LLM Token Math</title>
      <dc:creator>ThatToolSite</dc:creator>
      <pubDate>Tue, 13 Oct 2026 06:10:27 +0000</pubDate>
      <link>https://dev.to/thattoolsite/count-your-tokens-before-you-paste-practical-llm-token-math-4j5b</link>
      <guid>https://dev.to/thattoolsite/count-your-tokens-before-you-paste-practical-llm-token-math-4j5b</guid>
      <description>&lt;p&gt;You paste a long doc into ChatGPT, Claude or Gemini and one of three things happens: the answer ignores the end of it, you hit a context limit, or the API bill is bigger than you expected. All three come down to the same number: &lt;strong&gt;tokens&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's the practical version of token maths: what a token is, quick estimates you can do in your head, how to count exactly in a few lines of code, and how to work out what a prompt will cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a token actually is
&lt;/h2&gt;

&lt;p&gt;Models don't read characters or words. A tokenizer splits text into chunks from a fixed vocabulary, and the model sees a list of chunk IDs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Common English words are usually &lt;strong&gt;one token&lt;/strong&gt;: &lt;code&gt;the&lt;/code&gt;, &lt;code&gt;house&lt;/code&gt;, &lt;code&gt;token&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Longer or rarer words get &lt;strong&gt;split&lt;/strong&gt;: &lt;code&gt;tokenization&lt;/code&gt; → &lt;code&gt;token&lt;/code&gt; + &lt;code&gt;ization&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Spaces and punctuation count too. A leading space is often part of the next token (&lt;code&gt;world&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Numbers, code, URLs and non-Latin scripts use &lt;strong&gt;more tokens per character&lt;/strong&gt; than plain English.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is the one that surprises people. The same sentence in Hindi, Japanese or Arabic can take two or three times the tokens of its English version, and minified JSON or a stack trace is far denser than prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules of thumb (good enough for a quick check)
&lt;/h2&gt;

&lt;p&gt;For English prose with OpenAI's current tokenizers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;1 token ≈ 4 characters&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 token ≈ ¾ of a word&lt;/strong&gt;, so &lt;strong&gt;100 words ≈ 130 tokens&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A dense A4 page of text ≈ &lt;strong&gt;600–800 tokens&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Other providers publish their own rough ratios, and they differ a little because each model family has its own tokenizer. Treat these as estimates; for anything that matters, count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Count exactly in code
&lt;/h2&gt;

&lt;p&gt;OpenAI's tokenizers are open source. In JavaScript, &lt;code&gt;js-tiktoken&lt;/code&gt; runs anywhere, including the browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Tiktoken&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;js-tiktoken/lite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;o200k_base&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;js-tiktoken/ranks/o200k_base&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Tiktoken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o200k_base&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Count your tokens before you paste.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// number of tokens&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;o200k_base&lt;/code&gt; is the encoding used by GPT-4o and newer OpenAI models; older ones like GPT-4 and GPT-3.5 use &lt;code&gt;cl100k_base&lt;/code&gt;. In Python it's the &lt;code&gt;tiktoken&lt;/code&gt; package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;

&lt;span class="n"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_encoding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;o200k_base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Count your tokens before you paste.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Claude and Gemini the tokenizers aren't published, but both APIs have a token-counting endpoint (Anthropic's &lt;code&gt;count_tokens&lt;/code&gt; for messages, Gemini's &lt;code&gt;countTokens&lt;/code&gt;) if you need an exact number before sending a request.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a prompt will cost
&lt;/h2&gt;

&lt;p&gt;Every API price list is per &lt;strong&gt;million tokens&lt;/strong&gt;, and input and output are priced separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cost = input_tokens  / 1,000,000 × input_price
     + output_tokens / 1,000,000 × output_price
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worked example with made-up round prices, so the maths is easy to follow: say input costs &lt;strong&gt;$2&lt;/strong&gt; per million and output &lt;strong&gt;$8&lt;/strong&gt; per million. A 3,000-token prompt with a 500-token answer costs&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3,000 / 1,000,000 × $2 = $0.006
  500 / 1,000,000 × $8 = $0.004
                 total = $0.010 per request
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One cent sounds like nothing until it runs 50,000 times a day. Two things people forget:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Chat history is input every time.&lt;/strong&gt; In a long conversation, every new message resends the whole thread, so input tokens grow with each turn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output is usually the expensive side.&lt;/strong&gt; Asking for "brief" answers or a fixed format often saves more than trimming the prompt.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Five ways to cut tokens without losing quality
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Strip HTML and boilerplate&lt;/strong&gt; before you paste a web page. Navigation, footers and tags are pure token waste.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minify or trim JSON&lt;/strong&gt;: drop fields the model doesn't need instead of sending the whole API response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Summarise old context.&lt;/strong&gt; Replace the first 20 messages of a long thread with a five-line summary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't repeat instructions&lt;/strong&gt; in every message; put them once in the system prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check non-English and code-heavy inputs&lt;/strong&gt;, because they're where token counts surprise you most.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A quick way to check without writing code
&lt;/h2&gt;

&lt;p&gt;If you just want to paste something and see the count, I made a free &lt;a href="https://thattoolsite.com/token-counter?utm_source=devto&amp;amp;utm_medium=article" rel="noopener noreferrer"&gt;token counter&lt;/a&gt; that runs entirely in your browser, so nothing you paste is uploaded. It gives exact counts for OpenAI's tokenizers, clearly labelled estimates for Claude and Gemini, and a cost calculator with a dated, editable price table.&lt;/p&gt;

&lt;p&gt;What's the biggest token surprise you've run into: a huge chat history, a non-English document, or a JSON blob that ate the whole context window?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>openai</category>
      <category>beginners</category>
    </item>
    <item>
      <title>OCR in the browser with Tesseract.js: no server, no CDN</title>
      <dc:creator>ThatToolSite</dc:creator>
      <pubDate>Mon, 12 Oct 2026 16:15:50 +0000</pubDate>
      <link>https://dev.to/thattoolsite/ocr-in-the-browser-with-tesseractjs-no-server-no-cdn-3goe</link>
      <guid>https://dev.to/thattoolsite/ocr-in-the-browser-with-tesseractjs-no-server-no-cdn-3goe</guid>
      <description>&lt;p&gt;People OCR some of the most private things they own: ID cards, payslips, contracts, medical letters. Uploading those to a random "image to text" site never felt right, so for &lt;a href="https://thattoolsite.com" rel="noopener noreferrer"&gt;ThatToolSite&lt;/a&gt; I wanted OCR that runs entirely in the browser, with no server and not even a CDN.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/naptha/tesseract.js" rel="noopener noreferrer"&gt;Tesseract.js&lt;/a&gt; makes that possible. Here's what it took to make it fast, private and reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-host everything (yes, everything)
&lt;/h2&gt;

&lt;p&gt;Out of the box, Tesseract.js fetches its worker script, its WebAssembly core and the language data from a CDN. That works, but it means a third party sees every visitor who uses the tool, and the tool breaks if the CDN does. We serve all of it from our own domain under &lt;code&gt;/ocr&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;workerPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/ocr/worker.min.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;corePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;corePath&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;            &lt;span class="c1"&gt;// SIMD or plain build, see below&lt;/span&gt;
  &lt;span class="na"&gt;langPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/ocr/lang&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// tessdata_fast, gzipped&lt;/span&gt;
  &lt;span class="na"&gt;cachePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tts-ocr-fast-v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// IndexedDB key, bump when data changes&lt;/span&gt;
  &lt;span class="na"&gt;workerBlobURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;gzip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our browser test proves it: it reads an image and fails if &lt;strong&gt;any&lt;/strong&gt; request goes to another origin during OCR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick the right engine build for the device
&lt;/h2&gt;

&lt;p&gt;Tesseract ships a SIMD build of its WebAssembly core, which is noticeably faster where the CPU and browser support it, plus a plain fallback. We detect support with a tiny WebAssembly module that uses one SIMD instruction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SIMD_PROBE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;97&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;115&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;109&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;253&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;253&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;98&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hasWasmSimd&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;WebAssembly&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;WebAssembly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SIMD_PROBE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;corePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;simd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hasWasmSimd&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="s2"&gt;`/ocr/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;simd&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tesseract-core-simd-lstm.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tesseract-core-lstm.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Keep the download small
&lt;/h2&gt;

&lt;p&gt;Sizes matter when the whole engine runs on a phone:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WebAssembly core (SIMD or plain)&lt;/td&gt;
&lt;td&gt;about 2.9 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;English data (tessdata_fast, gzipped)&lt;/td&gt;
&lt;td&gt;1.97 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hindi, Spanish, French, German, Portuguese&lt;/td&gt;
&lt;td&gt;0.6 to 1.1 MB each&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We use the &lt;strong&gt;fast&lt;/strong&gt; language models, not the "best" ones: they're several times smaller, and for printed text the accuracy difference is small. Language data is cached in IndexedDB, so the second visit starts instantly, and a language is only downloaded when someone picks it. None of this loads until the visitor drops an image; the page itself stays light.&lt;/p&gt;

&lt;h2&gt;
  
  
  Help Tesseract see the text
&lt;/h2&gt;

&lt;p&gt;Most bad OCR results come from the image, not the engine. Two cheap fixes made the biggest difference:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scale to the size Tesseract likes.&lt;/strong&gt; It reads best when letters are roughly 20 to 40 pixels tall. Small screenshots get enlarged up to 2×, and huge phone photos are shrunk so the longest side is at most 4,000 px, which also keeps it quick.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An optional clean-up pass&lt;/strong&gt; for faint scans and dark-mode screenshots: convert to greyscale, stretch the contrast so the darkest 1% becomes black and the lightest 1% white, and flip light-on-dark images to dark-on-light. It's a few dozen lines of pixel code with no library, and it rescues a lot of "Tesseract read nothing" cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't block the page
&lt;/h2&gt;

&lt;p&gt;Recognition runs in Tesseract's own Web Worker, and we reuse one worker for every image. Calls are queued, so switching language waits for the current image instead of racing it. Progress events drive a real progress bar, and the test suite checks the page stays responsive while OCR runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Be honest about the limits
&lt;/h2&gt;

&lt;p&gt;Tesseract is excellent on printed text and weak on handwriting, and it doesn't keep table layout. The tool says that up front rather than letting people discover it after a bad result.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we test it
&lt;/h2&gt;

&lt;p&gt;The Playwright suite generates images with known text (including a dark-mode screenshot and a Spanish sentence with accents), runs them through the tool and compares the output. It also checks the worker, core and language files come from our own server.&lt;/p&gt;

&lt;p&gt;Try it on something private: &lt;a href="https://thattoolsite.com/image-to-text" rel="noopener noreferrer"&gt;Image to text, in your browser&lt;/a&gt;. It's free, and the image never leaves your device.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>webassembly</category>
      <category>privacy</category>
    </item>
    <item>
      <title>How we convert MP4 to MP3 entirely in the browser (WebCodecs + LAME)</title>
      <dc:creator>ThatToolSite</dc:creator>
      <pubDate>Sun, 11 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/thattoolsite/how-we-convert-mp4-to-mp3-entirely-in-the-browser-webcodecs-lame-3h0n</link>
      <guid>https://dev.to/thattoolsite/how-we-convert-mp4-to-mp3-entirely-in-the-browser-webcodecs-lame-3h0n</guid>
      <description>&lt;p&gt;Most "MP4 to MP3" sites upload your video to a server, convert it there and hand back a file. For a phone video of your kid's school concert, that's a lot of trust for a two-second job. So when I built &lt;a href="https://thattoolsite.com" rel="noopener noreferrer"&gt;ThatToolSite&lt;/a&gt;, I wanted the whole conversion to happen in the browser tab, with the file never leaving the device.&lt;/p&gt;

&lt;p&gt;It turned out to be very doable in 2026, with one catch: browsers can decode almost everything, but they can't &lt;strong&gt;encode&lt;/strong&gt; MP3. Here's how the pieces fit together.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://mediabunny.dev/" rel="noopener noreferrer"&gt;Mediabunny&lt;/a&gt;&lt;/strong&gt; reads and writes containers (MP4, MOV, WebM, MKV, M4A, OGG, MP3) and drives the browser's &lt;strong&gt;WebCodecs&lt;/strong&gt; API for decoding and encoding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebCodecs&lt;/strong&gt; (&lt;code&gt;AudioDecoder&lt;/code&gt;, &lt;code&gt;AudioEncoder&lt;/code&gt;) does the actual codec work, natively and fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@mediabunny/mp3-encoder&lt;/code&gt;&lt;/strong&gt;, an MP3 encoder based on &lt;strong&gt;LAME&lt;/strong&gt;, compiled to WebAssembly and plugged into Mediabunny as a custom encoder. (MPL-2.0; LAME itself is LGPL, credited on our About page.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Problem 1: WebCodecs has no MP3 encoder
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AudioEncoder.isConfigSupported({ codec: "mp3" })&lt;/code&gt; returns false in every major browser. AAC and Opus are fine; MP3 isn't there. The fix is to register LAME as an encoder that Mediabunny can use like a built-in one. We only load it when someone actually wants MP3, so it never weighs down the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;mp3Ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ensureMp3Encoder&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;mp3Ready&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@mediabunny/mp3-encoder&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerMp3Encoder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;mp3Ready&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before offering MP3 in the UI, we check it end to end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canMp3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ensureMp3Encoder&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;canEncodeAudio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mp3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sampleRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;44100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;numberOfChannels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bitrate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;128000&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If anything fails (an old browser, a blocked chunk), the MP3 option is simply disabled and M4A, WAV and OGG still work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: doing it without freezing the page
&lt;/h2&gt;

&lt;p&gt;Decoding a 10-minute video and re-encoding its audio is real work, so it runs in a &lt;strong&gt;Web Worker&lt;/strong&gt;. The page posts the &lt;code&gt;File&lt;/code&gt; to the worker (that shares a reference to the file's data rather than copying it) and gets progress messages back. With Mediabunny the conversion itself is short:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;conv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Conversion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                       &lt;span class="c1"&gt;// the video, opened from the File&lt;/span&gt;
  &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                      &lt;span class="c1"&gt;// an MP3 output writing to a Blob&lt;/span&gt;
  &lt;span class="na"&gt;tracks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;video&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;discard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;    &lt;span class="c1"&gt;// we only want the sound&lt;/span&gt;
  &lt;span class="na"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;codec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mp3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bitrateQuality&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;192&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// our helper: kbps to a Mediabunny quality&lt;/span&gt;
  &lt;span class="nx"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="c1"&gt;// optional start/end&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;conv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onProgress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;progress&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;conv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cancelling is an &lt;code&gt;AbortController&lt;/code&gt; passed into the job; the worker checks it between chunks and the UI shows "Cancelled" instead of a half-written file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: not making every visitor pay for it
&lt;/h2&gt;

&lt;p&gt;The media engine and the MP3 encoder are hundreds of kilobytes. Shipping them with every page would hurt the people who just wanted to read about the tool. Two changes fixed that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Load the engine on first use&lt;/strong&gt;, not at startup. Even the "does this browser support MP3?" check imports the engine lazily, after the page is idle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn off link prefetching&lt;/strong&gt; on pages that list many tools. Next.js prefetches linked routes in the viewport, so the homepage was quietly downloading every tool's engine.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Together these took the tool page's phone Lighthouse score from 86 to 99.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we test it
&lt;/h2&gt;

&lt;p&gt;Unit tests can't tell you whether an MP3 actually plays, so the browser test suite does the real thing in Playwright:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It generates a short test video in the browser (moving shapes plus a 440 Hz tone), encoded with Mediabunny itself.&lt;/li&gt;
&lt;li&gt;It drops the video on the tool and downloads the MP3.&lt;/li&gt;
&lt;li&gt;It checks the file starts with an ID3 tag or an MP3 frame sync, that the decoded duration matches the video, and that the dominant pitch is still about 440 Hz.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last check catches the bugs that matter: wrong sample rate, mangled channels, silence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits worth knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebCodecs support:&lt;/strong&gt; Chrome and Edge support everything here. Safari and Firefox support depends on the version and the codec, so the tool checks what the browser can do and says so instead of failing. Some video codecs (HEVC from iPhones) can only be decoded where the operating system has a decoder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's still your device doing the work.&lt;/strong&gt; A long 4K video takes longer on an old phone than on a server farm. For a privacy-first tool that's the right trade.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to see it working, the tool is here: &lt;a href="https://thattoolsite.com/mp4-to-mp3" rel="noopener noreferrer"&gt;MP4 to MP3, in your browser&lt;/a&gt;. It's free, and nothing is uploaded.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>webassembly</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
