<?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: Sitey</title>
    <description>The latest articles on DEV Community by Sitey (@siteypk).</description>
    <link>https://dev.to/siteypk</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%2F4177491%2Fb1846b6c-c5dc-432d-b1fe-c13aa153a5f3.png</url>
      <title>DEV Community: Sitey</title>
      <link>https://dev.to/siteypk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/siteypk"/>
    <language>en</language>
    <item>
      <title>Building a free Urdu AI voice with Gemini TTS: three bugs we hit</title>
      <dc:creator>Sitey</dc:creator>
      <pubDate>Sun, 11 Oct 2026 18:02:20 +0000</pubDate>
      <link>https://dev.to/siteypk/building-a-free-urdu-ai-voice-with-gemini-tts-three-bugs-we-hit-2ji4</link>
      <guid>https://dev.to/siteypk/building-a-free-urdu-ai-voice-with-gemini-tts-three-bugs-we-hit-2ji4</guid>
      <description>&lt;p&gt;We make Sitey, a small app for people in Pakistan who write in Urdu. In October we added a speaker button: every AI answer can be read aloud in Urdu, and you can send it to WhatsApp as a voice note. We also put the same voice on the web, free, at &lt;a href="https://app.sitey.pk/urdu-ai-voice/" rel="noopener noreferrer"&gt;app.sitey.pk/urdu-ai-voice&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Getting a voice to talk was quick. Making it work properly took longer. These are the problems we hit, in case you are building text-to-speech for a language that isn't English.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking the voice: listen, don't read benchmarks
&lt;/h2&gt;

&lt;p&gt;We first tried a different Urdu TTS service. Our founder listened and didn't like it. So we ran a small test: the same four lines through two Gemini TTS models and the tablet's own Android Urdu voice.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A line in Urdu script&lt;/li&gt;
&lt;li&gt;The same line in Roman Urdu (Urdu typed in English letters, which is how most people here text)&lt;/li&gt;
&lt;li&gt;An English line&lt;/li&gt;
&lt;li&gt;A mixed Urdu and English line, because that's how people actually talk&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Several Gemini voices each, saved as &lt;code&gt;.wav&lt;/code&gt; files in one folder, and a human listened to all of them. The whole test cost well under ten cents. The winner was &lt;strong&gt;Aoede&lt;/strong&gt; on &lt;code&gt;gemini-3.8-flash-lite-tts&lt;/code&gt;. It handled all four lines, and it says "Sitey" right.&lt;/p&gt;

&lt;p&gt;The mixed line was the useful one. Plenty of voices sound fine on clean Urdu and fall apart when an English brand name shows up in the middle of a sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: the voice read our instructions out loud
&lt;/h2&gt;

&lt;p&gt;In the voice test, a style instruction placed in the text (something like "Read this warmly") was read aloud by some voices as part of the message.&lt;/p&gt;

&lt;p&gt;The fix: send only the words.&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="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;generationConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;responseModalities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AUDIO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;speechConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;voiceConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;prebuiltVoiceConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;voiceName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Aoede&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same thing happened in the app with labels and emojis. A wishes card titled "Short and sweet" got its title read before the wish. An emoji was read as a random syllable (the cake 🎂 came out as "chha"). Now the speaker only gets the message text, and emojis are stripped first. There is a unit test for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: a loud noise instead of a voice
&lt;/h2&gt;

&lt;p&gt;Older examples return raw 16-bit PCM, and you wrap it in a 44-byte WAV header yourself. We did that, and the phones played a loud noise.&lt;/p&gt;

&lt;p&gt;Gemini now sends a finished WAV file, with a C2PA content-credentials chunk after the audio. We were putting a second WAV header around a file that already had one, so phones played the inner header and that chunk as sound.&lt;/p&gt;

&lt;p&gt;The fix is to check the first four bytes and pass a real WAV through untouched:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fromBase64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inlineData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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;isWav&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subarray&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;4&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RIFF&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isWav&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&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="na"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inlineData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;usage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// Otherwise it's raw PCM: read the rate from the mime type and add our own header.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bug 3: WhatsApp cut off the end of voice notes
&lt;/h2&gt;

&lt;p&gt;People sent the WAV to WhatsApp as a voice note, and the last part of the message was missing.&lt;/p&gt;

&lt;p&gt;We didn't fully work out whether WhatsApp or the trailing C2PA chunk was to blame, and we didn't need to. The app now re-encodes the audio on the phone to AAC &lt;code&gt;.m4a&lt;/code&gt; with Android's own encoder, drains the encoder to the end of the stream, and shares that. A device test checks that 3-second and 11.7-second notes keep their full length. On the tablet a real 4.60 s note came out as 4.61 s.&lt;/p&gt;

&lt;p&gt;If you share generated audio into a chat app, test the &lt;em&gt;received&lt;/em&gt; file's length, not just whether it plays.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping it free without going broke
&lt;/h2&gt;

&lt;p&gt;The voice runs on a Cloudflare Worker that holds the Gemini key; the app and the web page never see it. A few limits keep the cost predictable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The website gives 5 voices a day, up to 500 characters each, with a Cloudflare Turnstile check. The app gives 40 a day.&lt;/li&gt;
&lt;li&gt;There is a daily dollar cap on all AI calls, and a separate monthly cap for Gemini.&lt;/li&gt;
&lt;li&gt;If Gemini is busy, over its limit or offline, the app falls back to the phone's own Urdu voice. It sounds worse, but it still talks.&lt;/li&gt;
&lt;li&gt;We store the count and the cost of each call, never the text or the audio.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lite TTS pricing at the time of writing is $0.50 per million input tokens and $6.00 per million audio tokens. A short Urdu announcement costs a fraction of a cent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone starting this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Test with real mixed-language sentences, the way your users speak, and have a person listen.&lt;/li&gt;
&lt;li&gt;Send the model only the words you want spoken.&lt;/li&gt;
&lt;li&gt;Look at the first bytes of the audio before you wrap or convert it.&lt;/li&gt;
&lt;li&gt;Check the length of the audio after it goes through WhatsApp, not before.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can try the voice at &lt;a href="https://app.sitey.pk/urdu-ai-voice/" rel="noopener noreferrer"&gt;app.sitey.pk/urdu-ai-voice&lt;/a&gt; without signing up. Questions about Urdu TTS are welcome in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>android</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
