<?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: Servin Osmanov</title>
    <description>The latest articles on DEV Community by Servin Osmanov (@servin_osmanov).</description>
    <link>https://dev.to/servin_osmanov</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%2F3583111%2Fb3feb8b3-8b2e-4b44-96f8-41d587423dbf.jpg</url>
      <title>DEV Community: Servin Osmanov</title>
      <link>https://dev.to/servin_osmanov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/servin_osmanov"/>
    <language>en</language>
    <item>
      <title>My TTS read a whole novel. The model was the most reliable part of the pipeline</title>
      <dc:creator>Servin Osmanov</dc:creator>
      <pubDate>Fri, 09 Oct 2026 18:01:17 +0000</pubDate>
      <link>https://dev.to/servin_osmanov/my-tts-read-a-whole-novel-the-model-was-the-most-reliable-part-of-the-pipeline-595l</link>
      <guid>https://dev.to/servin_osmanov/my-tts-read-a-whole-novel-the-model-was-the-most-reliable-part-of-the-pipeline-595l</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/servin_osmanov/what-i-learned-building-speech-tech-for-a-language-with-zero-datasets-2gka"&gt;the first post of this series&lt;/a&gt; I mentioned, in a single table, that a synthesized voice had read an entire novel in Crimean Tatar: &lt;em&gt;Merdiven&lt;/em&gt; ("Staircase") by Şamil Alâdin, sixteen chapters, 1 hour 58 minutes of audio. In &lt;a href="https://dev.to/servin_osmanov/fine-tuning-was-the-easy-part-what-shipping-asr-for-a-low-resource-language-taught-me-4331"&gt;the second one&lt;/a&gt; I promised the story behind that table. Here it is.&lt;/p&gt;

&lt;p&gt;The short version, if you build anything like this: the model held up. The problems weren't in the model but in the text and audio preparation around it, which I'd written myself and could fix without retraining: splitting text into sentences, gluing short ones together, trimming breaths, pulling the book out of its file, checking the result. And a good share of those bugs simply cannot appear in a demo. They need a whole chapter, or a whole book, before they show up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thirty seconds of context
&lt;/h2&gt;

&lt;p&gt;Crimean Tatar has a sound, къ (a /q/), that ready-made models from neighbouring languages flatten into a plain k, so &lt;code&gt;qara&lt;/code&gt; ("black") comes out as &lt;em&gt;kara&lt;/em&gt;. I picked a multilingual base that had heard a lot of Kazakh, Uyghur and Bashkir, and I write our къ, гъ, нъ as the Bashkir letters ҡ, ғ, ң, which the model already knows. From there it reads our text as if it were Bashkir. That whole story is in &lt;a href="https://dev.to/servin_osmanov/what-i-learned-building-speech-tech-for-a-language-with-zero-datasets-2gka"&gt;the first post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The voice is a separate input. The model I fine-tuned learns the language, not the timbre. The timbre comes from a short reference clip, a few seconds of someone's recording with its exact transcript. &lt;em&gt;Merdiven&lt;/em&gt; is read in the voice of Sevil, and every sentence of the book was conditioned on one and the same seven-second clip. Keep that in mind, it comes back later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fourteen sentences can't tell you how a voice behaves for ten minutes
&lt;/h2&gt;

&lt;p&gt;Before the book, I compared model versions on fourteen probe sentences, stuffed on purpose with the hardest sounds we have: къ, гъ, нъ, дж. A fine regression set. It tells you nothing about whether the timbre drifts by the end of a chapter, what the model does with dialogue, names, numbers and one-word replies, or how it breathes. In a fifteen-second clip you don't hear any of that. In a book you hear all of it.&lt;/p&gt;

&lt;p&gt;So the next step was one full chapter: chapter one of &lt;em&gt;Merdiven&lt;/em&gt;, 1,608 words, about twelve minutes. The point was not to admire it. The point was to find what breaks.&lt;/p&gt;

&lt;p&gt;First I made sure the text wasn't in the training material: zero matching runs of six consecutive words against nearly 1.4 million words. Otherwise the model wouldn't be reading, it would be remembering. Later the whole book went through the same check, also with zero matches.&lt;/p&gt;

&lt;p&gt;Why this book? I love Şamil Alâdin's writing, and I wanted the first book a machine read in our language to be one of his. It also suited the model: even narrative prose. Humorous stories or children's books, where the voice has to play, were beyond it back then.&lt;/p&gt;

&lt;p&gt;The first number was already a lesson. On the probe sentences, about 14% of characters came out wrong when I read the synthesis back with our own speech recogniser and compared it to the text. On the chapter's ordinary prose, under 2%. The model hadn't improved overnight. The probe set is hostile by design and ordinary text isn't. I no longer quote the probe number as if it described how the model actually reads. It's a tripwire, not an estimate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fourteen sentences couldn't tell two fixes apart
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/servin_osmanov/what-i-learned-building-speech-tech-for-a-language-with-zero-datasets-2gka"&gt;the first post&lt;/a&gt; I told how I heard &lt;em&gt;ниже&lt;/em&gt; (Russian for "lower") where the book said &lt;code&gt;нидже&lt;/code&gt; ("how many"): my letter-mapping rule was also collapsing дж into ж, so the model was literally handed a Russian word. This is what happened next.&lt;/p&gt;

&lt;p&gt;I compared five ways of spelling that sound on the input. On the fourteen probe sentences the two best were indistinguishable. Both fixed the word. So I re-rendered the whole chapter, 195 sentences, with each of them. One, the Tatar letter җ, fixed &lt;code&gt;нидже&lt;/code&gt; and quietly damaged everything else: character errors across the chapter went up by 27%. Plain дж fixed the word and broke nothing.&lt;/p&gt;

&lt;p&gt;Fourteen sentences said the two candidates were equally good. A chapter showed that one of them hurts. A fix that has only been checked on the example that was failing hasn't really been checked. Run it over enough ordinary text to see what else it touches.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke in chapter one
&lt;/h2&gt;

&lt;p&gt;The verdict on chapter one surprised me. The voice held: the timbre barely changed from the first minute to the last, and the pace stayed even. The defects weren't in the model but in how text and audio were prepared around it. I'd written that part myself, which made it good news: none of it needed retraining.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gluing short sentences dropped whole clauses
&lt;/h3&gt;

&lt;p&gt;The model learned from clips no shorter than about three seconds, so I reasoned that a two-word line shouldn't go to it alone and glued short sentences to their neighbours. The logic felt airtight. I never tested it. That was a mistake.&lt;/p&gt;

&lt;p&gt;On glued chunks the model started throwing out entire sentences:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;asked to read&lt;/th&gt;
&lt;th&gt;what came out&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Men yañılğanım. O, oca degil eken.&lt;/td&gt;
&lt;td&gt;O, oca degil eken.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;– Yoq. Siz yañlışasıñız. Meni bir daa iç bir yerde köralmaycaqsıñız.&lt;/td&gt;
&lt;td&gt;Meni bir daa iç bir yerde köralmaycaqsıñız.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"I was wrong. He isn't a teacher after all" turned into just "He isn't a teacher after all". The glued chunks were a quarter of the chapter's text and produced 43% of its errors.&lt;/p&gt;

&lt;p&gt;Only then did I test the assumption itself and render the chapter with no gluing at all. Short sentences really are worse, roughly two and a half times the error rate, although in absolute terms that's about two percentage points. Errors keep falling until around seventy characters and barely move after that. And that penalty turned out smaller than the price of gluing, which was losing whole pieces of sentences. Gluing went off.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqm1o07mw2sourp11k5j0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqm1o07mw2sourp11k5j0.png" alt=" " width="799" height="491"&gt;&lt;/a&gt;&lt;br&gt;
Chapter one rendered without gluing. Everything under 43 characters used to be glued to a neighbour, but errors keep falling until about seventy.&lt;/p&gt;

&lt;p&gt;Before you build a workaround for something the model "can't handle", measure how badly it actually handles it. Here the workaround cost more than the problem it was meant to avoid.&lt;/p&gt;
&lt;h3&gt;
  
  
  The breath trimmer ate first words
&lt;/h3&gt;

&lt;p&gt;Sometimes the synthesis opens a sentence with an inhale, like a human narrator would. I wrote a trimmer: everything before the first voiced sound counts as breath and gets cut. But plenty of first words start with a voiceless consonant. &lt;em&gt;Çañ qaqıldı&lt;/em&gt; turned into &lt;em&gt;Qaqıldı&lt;/em&gt;, &lt;em&gt;Tışta, yeşergen…&lt;/em&gt; into &lt;em&gt;yeşergen…&lt;/em&gt;. In 111 of 147 clips the cut landed inside the first word. The chapter scored better with no trimming at all than with my "fix".&lt;/p&gt;

&lt;p&gt;The new trimmer looks at loudness instead of voicing. A consonant is quiet, but it's still far louder than the silence in front of it. With the new one, the chapter scores better than with no trimming.&lt;/p&gt;

&lt;p&gt;Benchmark every post-processing step against doing nothing. Mine lost, and I only found out because I ran that comparison.&lt;/p&gt;
&lt;h3&gt;
  
  
  The 89% that was the trimmer grading itself
&lt;/h3&gt;

&lt;p&gt;The first check reported that 89% of sentences opened with an inhale. Scary number. Then it turned out the trimmer was reporting on itself: it counted as breath everything before the first voiced sound, which included those same voiceless consonants. An honest measurement found a breath at the start of one sentence in five.&lt;/p&gt;

&lt;p&gt;Most of the real breaths were somewhere else entirely: between words, exactly where the trimmer never looked. 76 of 195 sentences had them. I learned to turn those down to the level of the background noise without moving a single timestamp, and 68 of the 76 sentences transcribe exactly as they did before.&lt;/p&gt;

&lt;p&gt;A component that defines what it detects will always agree with itself. And it only finds things where it looks.&lt;/p&gt;
&lt;h3&gt;
  
  
  Pace and pauses
&lt;/h3&gt;

&lt;p&gt;The model reads faster than a human narrator: 17.4 characters per second against 14.5. A chapter I expected to run fifteen minutes came out at twelve. For an audiobook that sounds a little hurried.&lt;/p&gt;

&lt;p&gt;The pauses between sentences I didn't make up. I measured them on ten minutes of a human narrator: the median pause between sentences was 288 milliseconds, and only three pauses in those ten minutes were longer than 0.6 seconds. So my pipeline puts 300 ms between sentences, a bit more when the speaker changes, and a bit more again between paragraphs.&lt;/p&gt;

&lt;p&gt;After all of this, chapter one went from 2.56% character errors to 1.84%. Same model, same weights. What changed was what I fed it and what I did to the audio afterwards.&lt;/p&gt;
&lt;h2&gt;
  
  
  Sixteen chapters: bugs that only exist at book length
&lt;/h2&gt;

&lt;p&gt;Then came the other fifteen chapters. I didn't re-render chapter one. It came out of the same pipeline, and after every fix below its extracted text was byte-for-byte identical to before. Which also means chapter one contained none of what came next, so a one-chapter test had no way to find it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The three asterisks that separate scenes, &lt;code&gt;* * *&lt;/code&gt;, went to the model as text, and it dutifully read them out.&lt;/li&gt;
&lt;li&gt;The last paragraph of the novel is the author's dateline, "1940 s.". My extraction took it for ordinary text, so the novel ended with a year read aloud.&lt;/li&gt;
&lt;li&gt;Five sentences were torn in half wherever a paragraph broke off on an ellipsis.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is the model. It's extraction, the least glamorous code in the whole pipeline. Run it over the entire text before you trust it on a sample.&lt;/p&gt;
&lt;h3&gt;
  
  
  A guard refused to build chapter seven, and it was right
&lt;/h3&gt;

&lt;p&gt;The packaging step has guards built in. In chapter seven one of them refused to build the chapter: the words of one sentence, joined back together, did not reproduce the original sentence. The cause was a double space in the book file.&lt;/p&gt;

&lt;p&gt;You can't hear a double space, and you can't see it on screen. But a reader that renders the text word by word would have lost it without a sound. It was the second time on this project that a program refusing to run caught something no amount of listening ever would.&lt;/p&gt;

&lt;p&gt;That incident also changed how the run behaves. If a guard fires in one chapter, the remaining chapters keep going. The failed chapter still doesn't get built, the rest just don't wait for it. A book that stops at chapter seven at night is a lost night.&lt;/p&gt;
&lt;h3&gt;
  
  
  The checker is a model too
&lt;/h3&gt;

&lt;p&gt;I checked every chapter separately, and one looked bad: chapter four, 8.3% character errors, four times the book's median. I went to listen, and the chapter was fine. The whole error came from one two-word line, "– dep tüşündi.", on which the checking recogniser got stuck in a loop and wrote it out twenty-five times in a row. Without that line the chapter is at about 2%, like the rest. The same line tripped the check in chapter two as well, it just stood out less there. The machine that read the book hadn't failed. The machine that checked it had.&lt;/p&gt;

&lt;p&gt;That's also why one average for the whole book would have been the wrong number. A single looping line wrecks the mean and says nothing about the reading. Per chapter the picture is flat: median 2.15%, best chapter 1.5%, worst 2.7%. Quality doesn't drift towards the end of the book or on long chapters. The longest, chapter two, runs nineteen and a half minutes and reads at 2.5%.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3md2c9p7n81kl931cqgs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3md2c9p7n81kl931cqgs.png" alt=" " width="800" height="514"&gt;&lt;/a&gt;&lt;br&gt;
Hatched: what checker glitches on short lines added, not the reading itself. Nearly all of it is one line, "– dep tüşündi.", which the recogniser looped on in chapter four and again in chapter two.&lt;/p&gt;

&lt;p&gt;If your evaluation runs on a model, it has failure modes of its own. Report per unit, keep the checker's failures visibly apart from the system's, and listen to an outlier before you believe it.&lt;/p&gt;

&lt;p&gt;On cost, as I wrote in &lt;a href="https://dev.to/servin_osmanov/what-i-learned-building-speech-tech-for-a-language-with-zero-datasets-2gka"&gt;the first post&lt;/a&gt;: two hours of audio take about three hours of my home computer. Synthesis itself is less than half of that. More goes into tying every word to a timestamp and checking everything. It's worth it.&lt;/p&gt;
&lt;h2&gt;
  
  
  A reader that highlights the current word
&lt;/h2&gt;

&lt;p&gt;I didn't want just an audio file. I wanted a reader: the text on screen, the voice reading it, the current word highlighted, and a click on any word jumps playback there.&lt;/p&gt;

&lt;p&gt;For that you need the time of every word. The trick the whole project started from helped again: align audio to text you already know. Only now the known text is exactly what the model was given as input. Each sentence is aligned on its own, against its own text, and then shifted into the chapter's timeline. In all sixteen chapters 100% of the words have a timestamp. The only things left without one are dashes and symbols that aren't pronounced.&lt;/p&gt;

&lt;p&gt;The doubtful timestamps aren't scattered at random. Most of them sit on the first word after a dialogue dash, which is exactly where the recognition check stumbles too. Two independent instruments point at the same spots. So a low word score is now my cue to listen to that sentence, not to delete the timestamp.&lt;/p&gt;

&lt;p&gt;Two more guards come from earlier mistakes. The timeline that had drifted 44 seconds by the end of a chapter is in &lt;a href="https://dev.to/servin_osmanov/what-i-learned-building-speech-tech-for-a-language-with-zero-datasets-2gka"&gt;the first post&lt;/a&gt;. Since then the build compares the end of the last sentence with the length of the audio file and refuses to run if they disagree by more than half a second. The other one is from the нидже story. I made the first build of the reader on the old narration, from before the дж fix, and the only symptom was the voice saying &lt;em&gt;ниже&lt;/em&gt;. Now every chapter stores a fingerprint of its audio file, so a stale narration shows up in the data and not only to someone listening carefully.&lt;/p&gt;

&lt;p&gt;Put together, the packaging step looks roughly like this:&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="c1"&gt;# a chapter is written only if these hold
&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sentence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;sentence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;    &lt;span class="c1"&gt;# words rebuild the sentence exactly
&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_sentence_end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;audio_seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;  &lt;span class="c1"&gt;# the timeline matches the audio
&lt;/span&gt;
&lt;span class="c1"&gt;# and the chapter records which audio it was built from
&lt;/span&gt;&lt;span class="n"&gt;chapter&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio_fingerprint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;content_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# stale narration is visible in data
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of these is clever. Two of them are only there because something had already got past me once.&lt;/p&gt;

&lt;p&gt;The reader is live on ana-yurt.com, one page for both reading and listening. You can open &lt;em&gt;Merdiven&lt;/em&gt; and listen to the whole thing: &lt;a href="https://ana-yurt.com/kutuphane/3444/read" rel="noopener noreferrer"&gt;ana-yurt.com/kutuphane/3444/read&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reference clip is an input too
&lt;/h2&gt;

&lt;p&gt;Since the timbre comes from the reference, picking a voice means picking a few seconds of audio. That clip turned out to matter more than I expected.&lt;/p&gt;

&lt;p&gt;I first tried my own voice on single sentences and heard long, out-of-place pauses in the middle of them. My first thought was the model. It was the reference: 0.77 seconds of silence before the words and 1.77 seconds after. The model takes more than timbre from the reference. It also takes the tempo, how much time to spend on each letter, and it was spending that extra time as pauses inside phrases. I trimmed the silence at the edges, and the median total pause inside a sentence fell from 930 to 85 milliseconds. In a blind comparison the voice with the trimmed reference couldn't be told apart from the old one: 7 to 6. The defect went away and the voice stayed.&lt;/p&gt;

&lt;p&gt;Then the voice started to sound dull to me. This time it was the microphone: a lavalier clipped to the chest sits off to the side of the mouth and loses exactly the frequencies that were missing, and a flat setting didn't help. No filter can paint that back in. What the mic didn't record isn't in the recording. I recorded the references again.&lt;/p&gt;

&lt;p&gt;I also tried to help the male voice through training, by adding more male speech to the training material. In a blind comparison the new version lost to the previous one, 3 to 11, and one of the female voices slid lower in pitch, from 208 to 199 Hz. I only added the pitch measurement after my ear had spoken, and both pointed at the same voice. That version didn't stay.&lt;/p&gt;

&lt;p&gt;If you build voice cloning into a pipeline, treat the reference clip like any other input data and check it. A bit of silence at its edges turned into pauses in the middle of every sentence.&lt;/p&gt;

&lt;p&gt;Once the pauses and the microphone were sorted out, I decided to try my own voice on a whole book. So the second book in our library, Yunus Qandım's &lt;em&gt;Küreş meydanını ot basmaz&lt;/em&gt;, 28 chapters, 6 hours 29 minutes of audio, was read by the machine in my voice. You can listen to it as well: &lt;a href="https://ana-yurt.com/kutuphane/3516/read" rel="noopener noreferrer"&gt;ana-yurt.com/kutuphane/3516/read&lt;/a&gt;. It was read by the current version of the model. &lt;em&gt;Merdiven&lt;/em&gt; was read by an earlier one.&lt;/p&gt;

&lt;p&gt;This book wasn't a random pick either. Its author, Yunus Qandım, is my uncle, and I really wanted his book to be heard in my voice.&lt;/p&gt;

&lt;p&gt;Usually, when you hear your own voice in a recording, it sounds foreign and a bit strange. This time it didn't. When I heard the first sentence, I was over the moon: the quality was very good.&lt;/p&gt;

&lt;h2&gt;
  
  
  What still doesn't work
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stress.&lt;/strong&gt; This one went in a circle, and I care about it more than the rest. I noticed the model sometimes reads the name &lt;em&gt;Riza&lt;/em&gt; as &lt;em&gt;Rıza&lt;/em&gt;, with a back vowel. A check confirmed it happens in about half the sentences, and the same goes for &lt;em&gt;Aziz&lt;/em&gt;. There was a fix: spell the vowel on the input as the Cyrillic «ий» instead of «и». The back vowel disappeared completely and the errors in the name went down. The instruments were happy.&lt;/p&gt;

&lt;p&gt;I listened blind and rejected it. My note on the blind page said: "if I hear И, the stress is wrong." The fix brought back the right vowel and dragged the stress to the first syllable, while ours sits on the last. Both versions were wrong, each in its own way.&lt;/p&gt;

&lt;p&gt;I wasn't too upset. These are first models, and I accept mistakes like this from them. I decided to come back to it later.&lt;/p&gt;

&lt;p&gt;Then I started measuring the stress itself. In our language the last syllable of a word is usually stretched a little, and human narrators stretch some words a lot and others hardly at all. On average the model stretches the last syllable as much as people do. But it reproduces only about six tenths of that range: it under-stretches exactly the words narrators stretch most and over-stretches the flat ones.&lt;/p&gt;

&lt;p&gt;So I went through the suspects one at a time. The reference clip changes only the overall tempo. The training recordings have the stretch where it belongs, so there was something to learn from. The inference settings change nothing. And the base model, before my fine-tuning, has the same flatness. The next suspect is how the model decides the length of the whole phrase up front. If the total duration is fixed in advance, the last syllable can only grow at its neighbours' expense. For now that's a hypothesis, and testing it means working on the model itself, not on the text that goes into it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Breaths.&lt;/strong&gt; Breath cleaning never made anything worse, but short breaths, around a tenth of a second, partly remain. My voice has the most: in one measurement breaths took up 15% of the audio, against about 1% for Sevil. Trimming the silence in the reference cut that by more than half, but not to zero.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pauses on emotional lines.&lt;/strong&gt; I had the impression that the model pauses longer where the text is more emotional. I tested that on sentences picked so that calm and emotional ones didn't differ in length or number of commas, and found no link. Pauses follow sentence length and commas, and emotional sentences in prose just tend to be longer. But I could only measure the total length of the pauses, not where they fall. A pause in the wrong place in an exclamation grates more than the same pause at a comma, so for me the question is still open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dialogue.&lt;/strong&gt; The model sees the dash before a reply, but it reads a change of speaker as a new sentence, not a new voice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Numbers.&lt;/strong&gt; I turn them into words beforehand, but recognition can't check how they were read: it writes down what it hears as digits, and "1924" comes back as "19124". For now only an ear checks the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole list, in one place
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;what broke&lt;/th&gt;
&lt;th&gt;what caught it&lt;/th&gt;
&lt;th&gt;what's in the pipeline now&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gluing short sentences dropped clauses&lt;/td&gt;
&lt;td&gt;reading the audio back and comparing it with the text&lt;/td&gt;
&lt;td&gt;no gluing; short lines cost less than lost ones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the breath trimmer cut into first words&lt;/td&gt;
&lt;td&gt;the same readback, and a no-trim baseline that won&lt;/td&gt;
&lt;td&gt;trimming on loudness, not voicing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"89% of sentences open with a breath"&lt;/td&gt;
&lt;td&gt;measuring apart from the trimmer&lt;/td&gt;
&lt;td&gt;breaths between words turned down, timestamps untouched&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;* * *&lt;/code&gt;, a dateline read aloud, five torn sentences&lt;/td&gt;
&lt;td&gt;the whole-book run&lt;/td&gt;
&lt;td&gt;fixed in extraction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a double space in the source file&lt;/td&gt;
&lt;td&gt;a guard that refused to build&lt;/td&gt;
&lt;td&gt;words must rebuild the sentence exactly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a timeline 44 seconds off by the end of a chapter&lt;/td&gt;
&lt;td&gt;an ear, playing single sentences&lt;/td&gt;
&lt;td&gt;timeline end vs audio length, half a second&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a stale narration in the reader&lt;/td&gt;
&lt;td&gt;an ear (&lt;em&gt;ниже&lt;/em&gt;)&lt;/td&gt;
&lt;td&gt;a fingerprint of every chapter's audio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;chapter four at 8.3%&lt;/td&gt;
&lt;td&gt;listening to the "bad" chapter&lt;/td&gt;
&lt;td&gt;per-chapter numbers, checker loops kept apart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;em&gt;Riza&lt;/em&gt; stressed on the wrong syllable&lt;/td&gt;
&lt;td&gt;an ear, blind&lt;/td&gt;
&lt;td&gt;still open&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The voice turned out to be the most reliable part of the whole chain. Nearly everything that broke was in the handling of text and audio around it: gluing, trimming, letters, timestamps, a double space. Some of it only instruments found, and no ear ever would have. Some of it only an ear found, and the instruments couldn't even score it: нидже, the stress in &lt;em&gt;Riza&lt;/em&gt;, breaths between words. And the whole book found things that neither fourteen sentences nor one chapter would ever have shown.&lt;/p&gt;

&lt;p&gt;When I finished listening to the book, I felt proud. The work hadn't been for nothing, and the result was excellent. But that's no reason to relax. You can still hear places in the book that need work, which means there's plenty of it ahead.&lt;/p&gt;




&lt;p&gt;Third post in a series. Next I'll go deeper into how I chose a base for the voice that can already say our къ, and why neighbouring Turkish doesn't work for that. After that, as promised, cloud GPUs.&lt;/p&gt;

&lt;p&gt;Audio samples and project page: &lt;a href="https://ai.ana-yurt.dev/" rel="noopener noreferrer"&gt;https://ai.ana-yurt.dev/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Want to help?&lt;/strong&gt; What the project needs most is native speakers. Listen to a chapter and note where the stress lands in the wrong place, where a name is misread, where a pause tears a phrase apart. You don't need to program, just a good ear for your own language. In this book the most important finds were made by ear. Write to &lt;a href="mailto:support@ana-yurt.com"&gt;support@ana-yurt.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you'd rather support the work financially, you can do it here: &lt;a href="https://ko-fi.com/anayurt" rel="noopener noreferrer"&gt;ko-fi.com/anayurt&lt;/a&gt;. That support turns into new recordings of living speech, and those need to be made now, while there are still people to learn from.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tts</category>
      <category>asr</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Fine-tuning was the easy part: what shipping ASR for a low-resource language taught me</title>
      <dc:creator>Servin Osmanov</dc:creator>
      <pubDate>Wed, 23 Sep 2026 19:49:45 +0000</pubDate>
      <link>https://dev.to/servin_osmanov/fine-tuning-was-the-easy-part-what-shipping-asr-for-a-low-resource-language-taught-me-4331</link>
      <guid>https://dev.to/servin_osmanov/fine-tuning-was-the-easy-part-what-shipping-asr-for-a-low-resource-language-taught-me-4331</guid>
      <description>&lt;p&gt;The fine-tune took an hour and a half on a single consumer GPU. Everything that made that hour and a half mean anything took months.&lt;/p&gt;

&lt;p&gt;This is the second post in a series about building speech tech for Crimean Tatar, a language with no datasets to speak of. &lt;a href="https://ai.ana-yurt.dev/blog/2026-09-kak-rodilas-ideya/" rel="noopener noreferrer"&gt;The first one&lt;/a&gt; covered synthesis and how the project started; this one is the recogniser — much less a story about training than about measurement.&lt;/p&gt;

&lt;p&gt;The headline, so you can decide whether to keep reading:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;WER&lt;/th&gt;
&lt;th&gt;CER&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;the model I started from&lt;/td&gt;
&lt;td&gt;34.6%&lt;/td&gt;
&lt;td&gt;11.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ fine-tuning (≈1.5 h, one GPU)&lt;/td&gt;
&lt;td&gt;20.1%&lt;/td&gt;
&lt;td&gt;9.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ tuning how it decodes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17.0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F03dw47dphjkxkw148o8x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F03dw47dphjkxkw148o8x.png" alt="Left to right: starting model, after fine-tuning, after decode-time tuning. The third bar cost zero training." width="799" height="491"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Left to right: starting model, after fine-tuning, after decode-time tuning. The third bar cost zero training.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now the parts worth your time.&lt;/p&gt;
&lt;h2&gt;
  
  
  I gave the model an exam before I tried to teach it
&lt;/h2&gt;

&lt;p&gt;I already had a "working" Crimean Tatar recogniser: a large Whisper, fine-tuned on our language, which I'd been using on clean studio audio where it looked fine. I had read its transcripts and nodded. I had never scored it.&lt;/p&gt;

&lt;p&gt;So the first thing I did here was not improve anything. I measured. 34.6% WER — every third word wrong. That's not "rough": it can't back archive search, can't produce training pairs for synthesis, and an assistant built on it mishears you constantly. I only knew that because I finally scored the thing I'd been eyeballing for a year.&lt;/p&gt;

&lt;p&gt;One footnote on that baseline: 34.6% slightly &lt;em&gt;understates&lt;/em&gt; it, because the model writes &lt;code&gt;1954&lt;/code&gt; where the book spells out &lt;code&gt;Biñ doquz yüz elli dört&lt;/code&gt; and the scorer counts four substitutions. Small, but if I don't say it, the number is dressed up.&lt;/p&gt;
&lt;h2&gt;
  
  
  Hold out whole recordings, not random clips
&lt;/h2&gt;

&lt;p&gt;Standard practice is a random 90/10 split. Here that would have been meaningless. Adjacent clips come from the same recording, same reader, same microphone, same afternoon — and the segmenter routinely cuts one sentence across two neighbouring clips, so a random split drops half a sentence in train and the other half in eval. What you measure then isn't language ability. It's whether the model memorised a voice and a room.&lt;/p&gt;

&lt;p&gt;So I held out &lt;strong&gt;two entire books, two readers the model had never heard&lt;/strong&gt;: 893 clips, 1 h 52 min. One move removes the voice, the mic, the recording era and the vocabulary of a particular text at once. If your data has structure — speaker, session, document, device — split on it. Random splitting is only safe when clips are genuinely independent, and in low-resource audio they essentially never are.&lt;/p&gt;

&lt;p&gt;That feels like enough caution. It wasn't.&lt;/p&gt;
&lt;h2&gt;
  
  
  Audit your eval set by content, not by filename
&lt;/h2&gt;

&lt;p&gt;Before launching anything I checked the obvious thing: is any of my held-out eval material also sitting in training?&lt;/p&gt;

&lt;p&gt;By filename: zero overlap. Not one collision. The two name sets looked like they came from different universes.&lt;/p&gt;

&lt;p&gt;Then I compared the &lt;em&gt;text&lt;/em&gt; instead: for every eval clip, does any training clip contain the same run of six consecutive words? Word 6-grams — cheap to build, cheap to hash, long enough that natural coincidence is rare at this corpus size.&lt;/p&gt;

&lt;p&gt;Four audiobooks were in my material twice. Same recordings, segmented by two different tools at two different times, under completely unrelated names.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;book&lt;/th&gt;
&lt;th&gt;share of its clips with a twin in training&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;book I'd picked for eval&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;96.9%&lt;/strong&gt; (651 / 672)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;second eval book&lt;/td&gt;
&lt;td&gt;58.8% (130 / 221)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;third book&lt;/td&gt;
&lt;td&gt;82.1% (769 / 937)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fourth book&lt;/td&gt;
&lt;td&gt;69.8% (549 / 787)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2tbvadkcp86erm570bvb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2tbvadkcp86erm570bvb.png" alt="Four books stored twice. Zero overlap by filename; the collision only showed up in the text." width="800" height="514"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Four books stored twice. Zero overlap by filename; the collision only showed up in the text.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Top row is the book I was about to grade the model on.&lt;/p&gt;

&lt;p&gt;I wasn't panicked. I just understood two things at once: every measurement I'd ever taken on this data was void, and thank God it surfaced before training rather than after.&lt;/p&gt;

&lt;p&gt;The counterfactual is the point. If I hadn't checked, nothing would have gone wrong. No crash, no warning, no weird loss curve. The model trains on recordings it will later be examined on — different cuts, same sentences, same voices — posts a great number, and I publish it. The number means nothing: the exam questions were handed out in advance. That's the nasty property of this bug class. &lt;strong&gt;It has no symptoms.&lt;/strong&gt; Everything works, the result is better than expected — which is exactly what should make you suspicious.&lt;/p&gt;

&lt;p&gt;And it generalises: &lt;strong&gt;in a low-resource language, re-cut copies of the same recordings are the norm, not the exception.&lt;/strong&gt; There is little material, so you keep returning to it with new tooling, and every pass produces fresh filenames wrapping old content. Deduplicate by content. Filenames are not identity.&lt;/p&gt;

&lt;p&gt;After cleaning I re-verified rather than trusting myself: zero eval clips in training, zero 6-gram matches. Measured and written down, not "I'm pretty sure I got them all".&lt;/p&gt;
&lt;h2&gt;
  
  
  Don't judge a fine-tune by its first checkpoint
&lt;/h2&gt;

&lt;p&gt;Now the part everyone thinks is the project, which was the fastest and dullest bit of it.&lt;/p&gt;

&lt;p&gt;I didn't retrain the model. I froze it and trained a small adapter alongside — &lt;strong&gt;2% of parameters&lt;/strong&gt;, 31M trainable against ~1.5B frozen. 15.5 hours of speech survived the cleanup, 3 epochs, 705 steps, &lt;strong&gt;1 h 30 min on one home GPU&lt;/strong&gt;, 10.4 GB peak VRAM, and a finished adapter of &lt;strong&gt;126 MB&lt;/strong&gt;. Result: 20.1% WER, 9.4% CER — a 42% relative cut in word errors.&lt;/p&gt;

&lt;p&gt;I felt nothing at the time; a number moved as designed. The pride arrived later, when I ran the model over material it had no business handling — films, podcasts, noisy field recordings — and the transcripts came back near-perfect. That's when it stopped being a table row and became working ears.&lt;/p&gt;

&lt;p&gt;Two things from the run are more useful than the headline number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkpoint 100 was worse than the base model.&lt;/strong&gt; I saved every 100 steps and scored all of them. The first checkpoint came out noticeably &lt;em&gt;worse&lt;/em&gt; than what I started from: the adapter has half-learned the new behaviour, it's already interfering with the old one, and it isn't giving anything back yet. If I'd glanced at it and concluded the approach was a dud, I'd have killed a run that ended up cutting error by 42%. A dip at the first checkpoint is what partial adaptation looks like, not what failure looks like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All the gains landed in epoch 1 of 3.&lt;/strong&gt; Past roughly step 200 the numbers stopped moving; epochs two and three contributed nothing measurable. So "1.5 hours" is itself padded — half an hour would have done. Checkpoint often and score often; runs converge long before the schedule says so.&lt;/p&gt;

&lt;p&gt;One detail that reassured me more than the average: both eval books improved, and the &lt;em&gt;smaller, later, different-reader&lt;/em&gt; one improved more. That's the signal you want — the model didn't just learn one narrator's delivery.&lt;/p&gt;
&lt;h2&gt;
  
  
  Decode-time wins are real — decompose them anyway
&lt;/h2&gt;

&lt;p&gt;Then error dropped again with no training at all.&lt;/p&gt;

&lt;p&gt;A trained model has knowledge, but it also has a &lt;strong&gt;decoding policy&lt;/strong&gt;. The default is greedy — take the locally most probable token, never reconsider. Fast, occasionally catastrophic: take one wrong turn and it can't back out, so it goes in circles. That's exactly what the worst eval clips were. Not mishearing — looping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Qıznıñ qıznıñ qıznıñ qıznıñ …
– dedi Akimoviç, – dedi Akimoviç, – dedi Akimoviç, …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is beam search: hold several candidate sequences and score whole hypotheses rather than committing token by token. Costs decode time, costs zero training. I swept 24 configurations on a separate selection set, not on the exam (more below), then ran the winner on the test set once. &lt;strong&gt;17.0% WER, 7.0% CER.&lt;/strong&gt; Not one weight changed. Decoding got 3.1× slower.&lt;/p&gt;

&lt;p&gt;Here it would have been easy to write "beam search: −3 WER points" and move on. True, and half the truth. So I broke the win down.&lt;/p&gt;

&lt;p&gt;409 word errors were recovered in total. &lt;strong&gt;160 of them came from 3 clips out of 893.&lt;/strong&gt; 0.34% of the material delivered 39% of the gain. Those are the loopers: on 41 reference words the greedy model emitted 175 words of repeated garbage. The remaining 890 clips improved by a modest, even 10%.&lt;/p&gt;

&lt;p&gt;Both halves are real, and neither alone justifies a 3.1× slowdown. But they mean different things. The broad 10% is "the model got more careful". The three clips are "it stopped occasionally losing its mind". For archive transcription the second matters far more, because a clip containing 500 bytes of one repeated token isn't merely bad — it poisons anything you train on it later. After the change: zero looping clips, 0 of 893.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decompose your aggregate wins.&lt;/strong&gt; If most of your improvement lives in a handful of examples, you didn't improve accuracy; you fixed a failure mode. Those call for different follow-up work, and averaging hides the difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  The obvious anti-loop knob broke real language
&lt;/h2&gt;

&lt;p&gt;There's a more direct tool against repetition: forbid the model from emitting the same n-gram twice. On paper, exactly the right hammer.&lt;/p&gt;

&lt;p&gt;I tested it. It was net harmful: across 255 selection clips it &lt;strong&gt;broke 17 and fixed 1&lt;/strong&gt;, and 12 of the 17 it broke had been essentially perfect. The transcripts explain themselves. The constraint doesn't stop the model repeating — it forces it to repeat &lt;em&gt;incorrectly&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;book        – Qartbaba, qartbaba, – tez-tez çapıp kelgen kiçkene Aziz
unrestricted– Qartbaba! Qartbaba! – tez-tez çapıp kelgen kiçkene Aziz
restricted  – Qartbaba! Qartbabaa! – tez-tez çapıp kelgen kiçkene Aziz
harder      – Qartbaba! Qartbava! – tez-tez çapıp kelgen kiçkene Aziz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A child runs up calling "Qartbaba, qartbaba!" — grandpa, grandpa. The model heard it perfectly. The ban wouldn't let it write down what it heard, so it corrupted the second word to avoid matching the first. Elsewhere &lt;code&gt;bağışla… bağışla…&lt;/code&gt; ("forgive me… forgive me…") came back the second time as the non-word &lt;code&gt;Bağuşla&lt;/code&gt;; in a third case the model routed around the constraint by inventing a suffix, &lt;code&gt;parçalarnıñ parça&lt;/code&gt; instead of &lt;code&gt;parçalar&lt;/code&gt;. Doubled address and doubled pleading are ordinary Crimean Tatar. People talk that way.&lt;/p&gt;

&lt;p&gt;This is my favourite finding of the stage, and it isn't really about decoding. &lt;strong&gt;You cannot fix a language with prohibitions.&lt;/strong&gt; Any rule of the form "this never occurs" eventually collides with how people actually speak, and it breaks precisely that spot while leaving everything else intact — so you won't notice. A decoding default borrowed from an English-language tutorial encodes assumptions about English. Test it against your own language's rhetoric first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three sets, and the test set gets decoded once per decision
&lt;/h2&gt;

&lt;p&gt;I've mentioned a "selection set" twice. Every number above rests on it. There are three sets, not two.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Train&lt;/strong&gt; — the model learns here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev / selection&lt;/strong&gt; — 255 clips, half an hour, two short whole works pulled out of the training material. This is where I &lt;em&gt;choose&lt;/em&gt;: which checkpoint, which of the 24 decode configs. Dozens of evaluations, as many as I liked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test&lt;/strong&gt; — the two held-out books. Decoded exactly &lt;strong&gt;twice all stage&lt;/strong&gt;: once on the winning checkpoint, once on the winning decode config. The selection rule was written down before I looked at any result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why so strict? Because if you run the test set repeatedly and take the best result each time, you're no longer selecting the best model — you're selecting the luckiest one. The number comes out pretty and never reproduces.&lt;/p&gt;

&lt;p&gt;The dev set got the same cleanliness audit: zero shared clips, zero 6-gram matches. After the duplicate story I don't file that under paranoia.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your dev set measures ranking, not absolute quality
&lt;/h2&gt;

&lt;p&gt;The dev set was &lt;strong&gt;much easier&lt;/strong&gt; than the test set. Same model: &lt;strong&gt;17% WER on dev, 34.6% on test.&lt;/strong&gt; Both honestly held out, neither seen in training. But the dev material comes from the same family of recordings as training, while the test books are two entirely foreign readers.&lt;/p&gt;

&lt;p&gt;So dev is good for "config A beats config B" and useless for "the model errs at X%". Report the dev number as your result and you're not lying exactly — you're reporting the easier exam. Keep both in their lanes: dev ranks, test reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool I couldn't validate gets no number
&lt;/h2&gt;

&lt;p&gt;One consequence of that discipline. Whisper ships a temperature-fallback mechanism against repetition: it detects circular output and retries. Precisely the tool this problem wants, and nearly free at decode time.&lt;/p&gt;

&lt;p&gt;But my dev set contained &lt;strong&gt;zero&lt;/strong&gt; looping clips. Nothing for it to act on — it looked like a config that does nothing at all. By my own selection rule I couldn't pick it, and I refused to sneak it onto the test set afterwards to see whether it would have helped. So I have no number for it, and I'm not inventing one. If you couldn't validate a tool on the set you're allowed to validate on, write down that it's unvalidated and keep it out of the results table.&lt;/p&gt;

&lt;h2&gt;
  
  
  17% in the studio. What about the world?
&lt;/h2&gt;

&lt;p&gt;Everything above was measured on one kind of audio: one person, one mic, quiet room, reading a book. Honest for its own world, and it says little about the world a voice assistant lives in. So I took the same 893 clips and corrupted them 17 ways — noise, music, competing speech, reverb, telephone-band filtering, time-stretching. The reference text is known, so error is measurable in each.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Telephone band and aggressive compression are free.&lt;/strong&gt; Through a telephone-band filter the model does a hair &lt;em&gt;better&lt;/em&gt; than in the studio. Speech rate barely matters either: ±15% tempo costs at most 6%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Competing speech is the enemy, by a wide margin.&lt;/strong&gt; Babble costs roughly 2× what steady noise at the same loudness costs, ~4× what music costs. At equal loudness with the target, WER goes from 17% to &lt;strong&gt;69%&lt;/strong&gt; — the model stops working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverb is second.&lt;/strong&gt; A hallway-sized room multiplies error by 2.5×.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That points somewhere specific: the highest-leverage investment in robustness is not more data. It's &lt;strong&gt;source separation and microphone placement.&lt;/strong&gt; More training hours will not fix a model whose input holds two people at equal volume.&lt;/p&gt;

&lt;p&gt;One last check that made me happy: on an open Crimean Tatar audiobook set the model had never trained on, clean chapters landed at essentially the same number as my own exam. It didn't memorise my readers. It hears the language.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd actually tell you
&lt;/h2&gt;

&lt;p&gt;Training was 90 minutes. The months went into auditing the eval set by content, splitting by recording, checkpointing and scoring instead of trusting the schedule, decomposing wins instead of quoting averages, refusing to let an English-shaped default constrain our grammar, and keeping a test set clean enough that its number means something.&lt;/p&gt;

&lt;p&gt;Fine-tuning is the part with a progress bar. It is not the part where the work is.&lt;/p&gt;




&lt;p&gt;Second post in a series. Coming up: a synthesised voice reading an entire book — sixteen chapters, nearly two hours of audio — and what had to be verified chapter by chapter. Then the one where I rented somebody else's GPUs and set money on fire, computing almost nothing.&lt;/p&gt;

&lt;p&gt;Audio samples and project page: &lt;a href="https://ai.ana-yurt.dev/en/blog/2026-09-mashina-nauchilas-slyshat/" rel="noopener noreferrer"&gt;https://ai.ana-yurt.dev/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Two ways to help. If you speak Crimean Tatar, the highest-value contribution is fifteen minutes with headphones — listen to a clip, say whether the machine wrote it down correctly, fix a transcript. No programming needed, and ears like that caught things here that no automated check did; write to &lt;a href="mailto:support@ana-yurt.com"&gt;support@ana-yurt.com&lt;/a&gt;. If you'd rather fuel the work directly, there's &lt;a href="https://ko-fi.com/anayurt" rel="noopener noreferrer"&gt;ko-fi.com/anayurt&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>deeplearning</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>What I learned building speech tech for a language with zero datasets</title>
      <dc:creator>Servin Osmanov</dc:creator>
      <pubDate>Thu, 10 Sep 2026 00:44:51 +0000</pubDate>
      <link>https://dev.to/servin_osmanov/what-i-learned-building-speech-tech-for-a-language-with-zero-datasets-2gka</link>
      <guid>https://dev.to/servin_osmanov/what-i-learned-building-speech-tech-for-a-language-with-zero-datasets-2gka</guid>
      <description>&lt;p&gt;The word was &lt;code&gt;qara&lt;/code&gt;, "black". I fed it to an off-the-shelf TTS model, hit play, and got back a soft, polite, very Turkish &lt;em&gt;kara&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;In Crimean Tatar къ and к are two separate sounds, and къ sits inside words we say every day: &lt;code&gt;yoq&lt;/code&gt;, &lt;code&gt;qız&lt;/code&gt;, &lt;code&gt;vaqıt&lt;/code&gt;, &lt;code&gt;qadar&lt;/code&gt;. Getting it wrong is not an accent. It is the model quietly speaking a neighbouring language and hoping nobody notices.&lt;/p&gt;

&lt;p&gt;I noticed. That one syllable is why I spent the next several months building speech models for Crimean Tatar on a single consumer GPU, in the evenings, with no dataset to start from. What follows is the engineering residue: what I would tell a solo developer about to do the same for their own low-resource language. Most of it cost me a week per lesson.&lt;/p&gt;

&lt;h2&gt;
  
  
  The chicken and the egg were both endangered
&lt;/h2&gt;

&lt;p&gt;The standard way to build a speech dataset is to take audio and run ASR over it to get text. Except we had no usable ASR for Crimean Tatar, and you cannot build good ASR without a corpus. Classic chicken and egg, except the chicken is also endangered.&lt;/p&gt;

&lt;p&gt;I stared at that loop longer than I want to admit before noticing I was holding the problem the wrong way round. Audiobooks come with the text. It is printed. It exists. There is nothing to recognise.&lt;/p&gt;

&lt;p&gt;So I stopped trying to &lt;em&gt;transcribe&lt;/em&gt; and started trying to &lt;em&gt;align&lt;/em&gt;: known text, audio, find where each word lands, cut on those boundaries. Forced alignment instead of recognition. That flip is the reason the project exists at all, because it sidesteps the exact thing the language did not have.&lt;/p&gt;

&lt;p&gt;If you take one thing from this post, take that one. Before training a recogniser, check whether the ground truth is already sitting on a shelf. If it is, you have an alignment problem, and alignment is enormously cheaper.&lt;/p&gt;

&lt;h2&gt;
  
  
  The run that lied to me for three and a half hours
&lt;/h2&gt;

&lt;p&gt;The first big alignment run covered seven and a half hours of audio. It ran, it did not error, and it produced clean, confident, well-formatted output.&lt;/p&gt;

&lt;p&gt;The first two hours were correct. The remaining three and a half hours were garbage. Not obviously-broken garbage either: plausible timestamps, plausible segments, looking exactly like the good ones. Out of seven hours I got a bit under two of usable material, and I only found out because I went listening.&lt;/p&gt;

&lt;p&gt;I have come to think of a crash as a gift. A crash points at the line. A process that returns confident nonsense takes a week off your life, slowly.&lt;/p&gt;

&lt;p&gt;What fixed it was not a code change but a granularity change. Instead of feeding the aligner three enormous blocks, I split the material into forty chapters and located each independently. Same audio, same text, same tooling, and over five hours of clean speech instead of under two.&lt;/p&gt;

&lt;p&gt;That is the rule I kept: if a stage can fail silently, never run it on big units. Small units fail visibly, and they let you bin the bad ones instead of the whole batch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Words do not line up. Letters do.
&lt;/h2&gt;

&lt;p&gt;That chapter-matching step had its own trap waiting. Locating a chapter inside a book sounds like a string search problem, and my first instinct was word-level similarity. It found nothing. I tested it on a pair I &lt;em&gt;knew&lt;/em&gt; matched, checked by hand, and the similarity came back under one percent.&lt;/p&gt;

&lt;p&gt;The reason will bite anyone working outside English. Crimean Tatar is agglutinative, so the "same" word carries different suffixes in the two sources. Add two alphabets in circulation (Cyrillic and Latin), and word tokens simply do not line up.&lt;/p&gt;

&lt;p&gt;Switching the comparison to short character sequences fixed it in one go: 38 of 40 chapters located. The characters survive the suffixes. The words do not. For anything morphologically rich or multi-script, character-level matching is the default and word tokens are an English-shaped assumption.&lt;/p&gt;

&lt;h2&gt;
  
  
  The base model had already decided
&lt;/h2&gt;

&lt;p&gt;Back to &lt;code&gt;qara&lt;/code&gt;. My first assumption was the obvious one: the model had never heard the sound. Easy to test, so I checked. In my data the letter q showed up in 88% of recordings, more often than k. The model had been shown the correct pronunciation thousands of times and had cheerfully ignored every one.&lt;/p&gt;

&lt;p&gt;The problem sat underneath the fine-tuning. In the base model, before it ever saw a word of my language, "q" was already wired to /k/ by English, Spanish and French. Fine-tuning was not overwriting that prior, it was decorating it. Which leads somewhere unpleasant: &lt;strong&gt;more data would not have helped&lt;/strong&gt;. Not ten more hours, not a hundred. The fix had to happen at the foundation.&lt;/p&gt;

&lt;p&gt;So I went base-model shopping with a different question. Not "which base speaks a language close to mine" but "which base already produces the sound I keep losing".&lt;/p&gt;

&lt;p&gt;Everybody's first suggestion is Turkish, the big well-resourced relative. But Turkish has no /q/ at all, only one k, which is exactly how you get &lt;em&gt;kara&lt;/em&gt;. I verified that twice, because I did not trust my own ears the first time. Azerbaijani looks promising until you listen: its q leans towards /g/, then softens further into something breathy. The languages that keep the sound, with a dedicated letter of its own, are Kazakh, Uyghur and Bashkir.&lt;/p&gt;

&lt;p&gt;The multilingual base I ended up choosing had roughly 1500 hours of Kazakh, 400 of Uyghur and 250 of Bashkir in its training mix, against 125 hours of Turkish. Exactly upside down from what "pick the closest big language" would tell you.&lt;/p&gt;

&lt;p&gt;It still had no Crimean Tatar in it. I got around that at the writing-system level: our къ, гъ and нъ each map onto a single letter the model already knows (ҡ, ғ, ң), so you hand it Crimean Tatar text and it reads something familiar. One trick, two problems.&lt;/p&gt;

&lt;p&gt;The nice part is that this is verifiable &lt;em&gt;before&lt;/em&gt; you spend a single GPU-hour on training. Out of the box, with no fine-tuning at all, the new base pronounced our къ correctly 88–100% of the time. I was no longer teaching the model a new sound. I had stopped fighting it.&lt;/p&gt;

&lt;p&gt;I took the Kazakh and Bashkir variants all the way to the end, because I wanted the comparison rather than the argument, and they tied to the third decimal place. I shipped Bashkir: at a certain point in training the Kazakh one returned a few empty phrases and let the /q/ slide back towards k. Given equal numbers, I take the one that breaks less often.&lt;/p&gt;

&lt;p&gt;For a language with no data of its own, picking the base is a phonetics decision, not a taxonomy one. And you can smoke-test the prior in an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three times the data, and the number would not move
&lt;/h2&gt;

&lt;p&gt;My favourite negative result of the project. The corpus kept growing: 5.9 hours, then 11.5, then 15.3. I tracked character error rate by synthesising speech and running it back through a recogniser, averaged over three runs each:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;corpus&lt;/th&gt;
&lt;th&gt;CER (mean of 3)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;base, no Crimean Tatar at all&lt;/td&gt;
&lt;td&gt;0.173–0.198&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5.9 h&lt;/td&gt;
&lt;td&gt;0.136&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11.5 h&lt;/td&gt;
&lt;td&gt;0.138&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15.3 h&lt;/td&gt;
&lt;td&gt;0.141&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two and a half times the data and the number goes slightly &lt;em&gt;worse&lt;/em&gt;. Meanwhile the run-to-run spread on one unchanged voice was wide enough to swallow that whole difference, so the honest reading is that all three runs are indistinguishable by this metric. And yet in blind listening, not knowing which sample was which, I picked the newer voice every time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Folkqgphiwee0k1v8c9vc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Folkqgphiwee0k1v8c9vc.png" alt="Corpus size against automatic quality metric" width="799" height="491"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The corpus grew two and a half times over. The automatic metric flatlined. Blind listening kept preferring the newer voice anyway.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The explanation matters more than the numbers. Intelligibility saturates early, around six hours. Prosody, breathing, the liveliness of a sentence keep improving well past the point where CER stops moving. The metric goes quiet exactly where the remaining quality lives, so anyone reading that table alone would have stopped collecting data, reporting the truth while making the wrong call.&lt;/p&gt;

&lt;p&gt;The cheaper form of the same lesson: one measurement is not a measurement. The same voice gave one number on the first run and a noticeably different one averaged over three. I have deleted several "wins" that turned out to be variance wearing a hat. Measure three times, and never give a single automatic metric the only vote.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug you can only hear if you click one sentence
&lt;/h2&gt;

&lt;p&gt;I was listening to a narrated chapter, clicked on sentence 195 to check something, and heard the wrong sentence. Not slightly wrong, a different sentence. The drift was beautifully linear:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;sentence&lt;/th&gt;
&lt;th&gt;timestamp offset&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;+10.7 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;98&lt;/td&gt;
&lt;td&gt;+21.8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;147&lt;/td&gt;
&lt;td&gt;+33.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;195&lt;/td&gt;
&lt;td&gt;+43.9 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My first theory was the fashionable one: the model falls apart over long text. Wrong, and worth checking rather than assuming. Speaker similarity to the opening held at 0.96–0.98 throughout, and the tempo never shifted. The audio was fine.&lt;/p&gt;

&lt;p&gt;The actual cause: two parts of my pipeline computed time independently, and one counted the pauses between phrases twice. Forty-three seconds is exactly the sum of those double-counted pauses across the chapter. Correct audio, wrong labels.&lt;/p&gt;

&lt;p&gt;I did not debug this by reading code. I debugged it with a knife: cut the audio at the old timestamps and at the new ones, push both through recognition, compare. New marks came back at CER 0.008, so the text matches. Old marks came back at 0.712, so those are other sentences entirely. On the final line the old timestamp had run past the end of the file, and the recogniser dutifully "heard" its own boilerplate captions in the silence.&lt;/p&gt;

&lt;p&gt;The part that should worry you: played straight through, the file sounds perfect. Every consumer of those timestamps, a word-highlighting reader or a training pipeline, would have inherited garbage from a file that passes any listening test you can run. Whole-file playback validates whole-file playback and nothing else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The exam the model had already seen
&lt;/h2&gt;

&lt;p&gt;The ASR side of the project produced numbers I liked:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;WER&lt;/th&gt;
&lt;th&gt;CER&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;the model I started from&lt;/td&gt;
&lt;td&gt;34.6%&lt;/td&gt;
&lt;td&gt;11.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ fine-tuning (about 1.5 h on one GPU)&lt;/td&gt;
&lt;td&gt;20.1%&lt;/td&gt;
&lt;td&gt;9.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ tuning how the model decodes its answer&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17.0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkm003q1l3ulwzc94f61m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkm003q1l3ulwzc94f61m.png" alt="ASR word and character error rates before and after" width="799" height="491"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Left to right: starting model, after fine-tuning, after decoding changes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The third row involved no training at all, not a single weight touched, only a more careful decoding setup, and it moved WER by three points. The entire adapter I added to that model is 126 MB.&lt;/p&gt;

&lt;p&gt;Now the part that nearly ruined it. The same four audiobooks existed twice in my material. Cut differently, segmented differently, stored under unrelated filenames, zero overlap by name. For one book I had picked as an eval set, 96.9% of the segments had a twin sitting in the training data.&lt;/p&gt;

&lt;p&gt;If I had missed it, I would have been examining the model on questions it had already been handed the answers to. A lovely number, published, believed, with the next six months built on top.&lt;/p&gt;

&lt;p&gt;Filenames are not identity. I check for near-duplicates by content now, always, and I treat a suspiciously good eval score as a bug report. Same reflex later, when a synthesised voice read a whole book: I proved by measurement, not by reasoning, that the book's text never entered training. Zero word-sequence matches against a corpus of nearly one and a half million words. Otherwise it is not reading, it is reciting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs now
&lt;/h2&gt;

&lt;p&gt;A synthesised voice read an entire book, &lt;code&gt;Merdiven&lt;/code&gt; by Şamil Alâdin. Not a demo paragraph. All sixteen chapters, 15,444 words, one hour fifty-eight minutes of audio, word timings verified across 100% of the words, per-chapter CER median 0.022 (worst 0.027, best 0.015).&lt;/p&gt;

&lt;p&gt;The timings got a control that was required to fail: cut 30 words per chapter at the recorded marks and recognise them, 447 of 480 matched; shift the same fragments by 0.4 seconds and 4 of 480 match. That gap is what makes the check worth running.&lt;/p&gt;

&lt;p&gt;And the number I care about most: roughly three hours of machine time per two hours of finished audio, everything included. An audiobook in Crimean Tatar now costs one night of one home computer. Not a studio, not a year of waiting.&lt;/p&gt;

&lt;p&gt;Earlier work is public if you want a starting point: the &lt;a href="https://huggingface.co/datasets/servinosmanov/tts-crh-sevil-fixed" rel="noopener noreferrer"&gt;dataset&lt;/a&gt; and the &lt;a href="https://huggingface.co/servinosmanov/xtts-crh-sevil-v1" rel="noopener noreferrer"&gt;model&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the months actually went
&lt;/h2&gt;

&lt;p&gt;Training was hours. An hour and a half here, a few hours there. It is not the line item anybody thinks it is.&lt;/p&gt;

&lt;p&gt;The months went into collecting and aligning data, repairing the parts that broke without saying anything, proving the evaluation was honest, re-measuring, and listening with headphones, blind, one sentence at a time.&lt;/p&gt;

&lt;p&gt;That last one produced the finding I did not expect. Across the project the bugs split roughly half and half between instruments and a human ear. Instruments caught the drifting timestamps, which no amount of listening would ever have revealed. The ear caught breaths in wrong places, bad annotations, and this one:&lt;/p&gt;

&lt;p&gt;I heard &lt;em&gt;ниже&lt;/em&gt; where the book said &lt;code&gt;нидже&lt;/code&gt;. The cause took a minute to find. My letter-mapping rule, the same trick that gave me ҡ and ғ, was also collapsing дж into ж. The model had been handed the string "ниже" and pronounced it flawlessly. Perfect execution of a wrong input.&lt;/p&gt;

&lt;p&gt;The rule had erased a distinction the language genuinely has: дж in our own words like &lt;code&gt;джан&lt;/code&gt;, &lt;code&gt;оджа&lt;/code&gt;, &lt;code&gt;нидже&lt;/code&gt;, versus ж in loanwords. After that collapse nothing downstream could have said them differently. That chapter held fifty-five such words. I caught exactly one, the case that happened to land on another real word. The other fifty-four sounded "a bit off", and the ear slides right over "a bit off".&lt;/p&gt;

&lt;p&gt;Fixing it barely moved any metric. I fixed it anyway, because it is our language, not a leaderboard.&lt;/p&gt;

&lt;p&gt;Which is the most useful thing I can hand anyone doing this for their own language: your cheapest and strongest tool is one native speaker, fifteen minutes and headphones. It beats the automation, and it means you do not have to be a programmer to contribute. You need to hear your language well.&lt;/p&gt;




&lt;p&gt;First post in a series. Coming up: teaching a recogniser to hear Crimean Tatar; narrating a book chapter by chapter; running blind A/B tests so you stop fooling yourself; and the one where I set money on fire renting somebody else's GPUs and computed almost nothing.&lt;/p&gt;

&lt;p&gt;Voice samples and project page: &lt;a href="https://ai.ana-yurt.dev/blog/2026-09-kak-rodilas-ideya/" rel="noopener noreferrer"&gt;https://ai.ana-yurt.dev/blog/2026-09-kak-rodilas-ideya/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Two ways to help. If you're a Crimean Tatar speaker, the highest-value contribution is fifteen minutes with headphones — blind listening and proofreading beat any tool in my stack; write to &lt;a href="mailto:support@ana-yurt.com"&gt;support@ana-yurt.com&lt;/a&gt;. If you'd rather fuel the work directly, there's &lt;a href="https://ko-fi.com/anayurt" rel="noopener noreferrer"&gt;ko-fi.com/anayurt&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>lm</category>
      <category>tts</category>
      <category>programming</category>
    </item>
    <item>
      <title>My Journey Improving a TTS Model for the Crimean Tatar Language</title>
      <dc:creator>Servin Osmanov</dc:creator>
      <pubDate>Fri, 07 Nov 2025 19:42:08 +0000</pubDate>
      <link>https://dev.to/servin_osmanov/my-journey-improving-a-tts-model-for-the-crimean-tatar-language-53f7</link>
      <guid>https://dev.to/servin_osmanov/my-journey-improving-a-tts-model-for-the-crimean-tatar-language-53f7</guid>
      <description>&lt;p&gt;When you work with machine learning, success often hides behind hours of frustration, countless errors, and broken pipelines. This project — improving the &lt;strong&gt;Crimean Tatar TTS (Text-to-Speech)&lt;/strong&gt; model — was exactly that kind of journey. What started as a small experiment to fine-tune an existing model turned into a full-scale debugging adventure that taught me more about data integrity, audio processing, and patience than any tutorial could.&lt;/p&gt;

&lt;p&gt;In my previous article, &lt;a href="https://dev.to/servin_osmanov/why-language-tech-matters-developing-ai-tools-for-small-languages-583h"&gt;Why Language Tech Matters: Developing AI Tools for Small Languages&lt;/a&gt;, I explored how AI can empower low-resource languages. This piece continues that journey with a hands-on look at improving TTS models.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu4vpl5k2r345s9eueqho.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu4vpl5k2r345s9eueqho.png" alt="Sevil tts model improvement" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Starting Point: A Model That Worked — but Only Partially
&lt;/h2&gt;

&lt;p&gt;My goal was simple: improve the voice model “&lt;strong&gt;Sevil&lt;/strong&gt;” for the Crimean Tatar language. I had already worked with similar voices — “&lt;strong&gt;Arslan&lt;/strong&gt;” and “&lt;strong&gt;Abibullah&lt;/strong&gt;” — using Hugging Face datasets like &lt;code&gt;speech-uk/tts-crh-arslan&lt;/code&gt; and &lt;code&gt;speech-uk/tts-crh-abibullah&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first training attempts went well — &lt;strong&gt;loss around 0.283&lt;/strong&gt;, results acceptable. But something didn’t add up. The dataset had &lt;strong&gt;1,566 audio files&lt;/strong&gt;, yet the training logs showed only &lt;strong&gt;415 were being used&lt;/strong&gt; — about &lt;strong&gt;26.5%&lt;/strong&gt; of the total.&lt;br&gt;&lt;br&gt;
That meant &lt;strong&gt;almost three-quarters of my data was silently ignored&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At first, I thought it was a fluke. Then I realized it was a &lt;strong&gt;systemic problem&lt;/strong&gt; in the Hugging Face &lt;code&gt;datasets&lt;/code&gt; API when loading compressed audio from Parquet files.  &lt;/p&gt;
&lt;h2&gt;
  
  
  Diagnosing the Problem
&lt;/h2&gt;

&lt;p&gt;When I loaded the dataset through &lt;code&gt;datasets.load_dataset()&lt;/code&gt;, most files threw errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: "Error while decoding audio"
Error: "Audio file appears to be empty"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That didn’t make sense — the audio bytes were clearly present in the Parquet files.&lt;br&gt;&lt;br&gt;
After checking manually with &lt;code&gt;pandas.read_parquet()&lt;/code&gt;, I confirmed the data was there. The problem wasn’t in the files — it was in how the decoder handled them.&lt;/p&gt;

&lt;p&gt;That’s when I realized: the &lt;strong&gt;datasets API couldn’t decode raw audio bytes correctly&lt;/strong&gt;. The data was fine, but the pipeline was broken.&lt;/p&gt;
&lt;h2&gt;
  
  
  Turning Bytes into Sound
&lt;/h2&gt;

&lt;p&gt;At this point, I tried everything.&lt;br&gt;&lt;br&gt;
I extracted the bytes manually, saved them as &lt;code&gt;.raw&lt;/code&gt; files, and tried to load them with &lt;code&gt;librosa&lt;/code&gt; and &lt;code&gt;soundfile&lt;/code&gt;. Nothing worked.&lt;br&gt;&lt;br&gt;
Without proper &lt;strong&gt;metadata&lt;/strong&gt; (sample rate, channels, encoding), the files were unreadable.&lt;/p&gt;

&lt;p&gt;Eventually, I discovered the solution: &lt;strong&gt;FFmpeg&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;By using known dataset parameters —&lt;br&gt;&lt;br&gt;
&lt;code&gt;sample rate: 16000 Hz&lt;/code&gt;, &lt;code&gt;channels: mono&lt;/code&gt;, &lt;code&gt;format: PCM 16-bit little-endian&lt;/code&gt; —&lt;br&gt;&lt;br&gt;
I could convert all &lt;code&gt;.raw&lt;/code&gt; files into clean &lt;code&gt;.wav&lt;/code&gt; audio.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-f&lt;/span&gt; s16le &lt;span class="nt"&gt;-ar&lt;/span&gt; 16000 &lt;span class="nt"&gt;-ac&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;-i&lt;/span&gt; sevil_0000.raw &lt;span class="se"&gt;\&lt;/span&gt;
       sevil_0000.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And just like that — &lt;strong&gt;1,566 files successfully converted&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
No corruption. No decoding errors. 100% validation success.&lt;/p&gt;

&lt;p&gt;It was the moment of breakthrough — the kind that makes you sit back, smile, and realize you’ve just solved a problem that haunted you for two days straight.&lt;/p&gt;
&lt;h2&gt;
  
  
  Training the Model — Again
&lt;/h2&gt;

&lt;p&gt;With clean audio finally ready, I retrained the Sevil model from scratch, this time using &lt;strong&gt;all 1,566 recordings&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Training setup (based on my previous configs for “Arslan”):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;num_train_epochs = 500
batch_size = 4
learning_rate = 1e-4
fp16 = True
warmup_steps = 2000
save_steps = 2000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The progress was promising:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Loss dropped from &lt;strong&gt;1.14 → 0.80 → 0.50 → 0.27&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The voice quality improved with every epoch
&lt;/li&gt;
&lt;li&gt;And then… it crashed at &lt;strong&gt;78%&lt;/strong&gt; completion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The culprit? A familiar one for SpeechT5 users —&lt;br&gt;&lt;br&gt;
&lt;code&gt;RuntimeError: torch.cat(): expected a non-empty list of Tensors&lt;/code&gt;&lt;br&gt;&lt;br&gt;
It turned out to be a bug in &lt;strong&gt;guided_attention_loss&lt;/strong&gt;, a component that sometimes fails with uneven sequence lengths.&lt;/p&gt;
&lt;h2&gt;
  
  
  Fixing the Crash
&lt;/h2&gt;

&lt;p&gt;Instead of starting over, I resumed training from the last checkpoint (step 16,000) and simply &lt;strong&gt;disabled guided attention&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model.config.use_guided_attention_loss = False
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one line saved the project.&lt;br&gt;&lt;br&gt;
Training resumed, completed &lt;strong&gt;98% of the full cycle&lt;/strong&gt;, and stabilized with a final &lt;strong&gt;loss of 0.267&lt;/strong&gt; — a small numerical improvement, but a big qualitative one.&lt;br&gt;&lt;br&gt;
The model became more consistent and robust across new data.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Data Used&lt;/th&gt;
&lt;th&gt;Loss&lt;/th&gt;
&lt;th&gt;Success&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sevil v1&lt;/td&gt;
&lt;td&gt;415 files (26.5%)&lt;/td&gt;
&lt;td&gt;0.276&lt;/td&gt;
&lt;td&gt;47% test&lt;/td&gt;
&lt;td&gt;Trained on partial data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sevil v2&lt;/td&gt;
&lt;td&gt;1,566 files (100%)&lt;/td&gt;
&lt;td&gt;0.267&lt;/td&gt;
&lt;td&gt;100% test&lt;/td&gt;
&lt;td&gt;Fully trained, stable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The difference wasn’t just in metrics — it was in &lt;strong&gt;confidence&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
Sevil v2 generalized better, produced smoother intonation, and maintained pronunciation consistency even on unseen words.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Never trust good metrics without checking data coverage.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
My “good” baseline was trained on just 26% of the data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;FFmpeg is a lifesaver.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
It solved what specialized libraries couldn’t.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Validate every single file.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Automation saved hours of manual checking.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Guided attention loss is optional.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Sometimes stability matters more than theoretical accuracy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Document everything.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
By keeping track of every attempt — successful or not — I could understand the full story, not just the happy ending.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why This Project Matters
&lt;/h2&gt;

&lt;p&gt;For me, this wasn’t just about fixing one dataset. It was about enabling a &lt;strong&gt;low-resource language&lt;/strong&gt; — Crimean Tatar — to have a better voice in the digital world.&lt;br&gt;&lt;br&gt;
Improving the Sevil model means clearer pronunciation, smoother prosody, and better accessibility for learners and native speakers alike.&lt;/p&gt;

&lt;p&gt;And for anyone working with custom TTS datasets:&lt;br&gt;&lt;br&gt;
Check your files, validate your data, and don’t give up when your model crashes at 78%.&lt;br&gt;&lt;br&gt;
That crash might be the best teacher you’ll ever have.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Author:&lt;/strong&gt; &lt;em&gt;Servin Osmanov&lt;/em&gt;&lt;br&gt;&lt;br&gt;
Lead Fullstack Python / ReactJS Engineer&lt;br&gt;&lt;br&gt;
AI researcher and TTS developer for low-resource languages&lt;br&gt;&lt;br&gt;
Project: &lt;code&gt;servinosmanov/tts-crh-sevil-fixed&lt;/code&gt; on Hugging Face&lt;/p&gt;

</description>
      <category>ai</category>
      <category>huggingface</category>
      <category>programming</category>
      <category>nlp</category>
    </item>
    <item>
      <title>My Journey as a Judge at CBIT Hacktoberfest 2025 — Lessons from the Other Side of the Table</title>
      <dc:creator>Servin Osmanov</dc:creator>
      <pubDate>Thu, 30 Oct 2025 18:29:37 +0000</pubDate>
      <link>https://dev.to/servin_osmanov/my-journey-as-a-judge-at-cbit-hacktoberfest-2025-lessons-from-the-other-side-of-the-table-78e</link>
      <guid>https://dev.to/servin_osmanov/my-journey-as-a-judge-at-cbit-hacktoberfest-2025-lessons-from-the-other-side-of-the-table-78e</guid>
      <description>&lt;p&gt;When I first started attending hackathons, I was always the one coding, building, and pitching. This year, for the first time, I got to experience the other side — as a &lt;strong&gt;judge&lt;/strong&gt; at the &lt;a href="https://cbit-hacktoberfest25.devpost.com/?ref_feature=challenge&amp;amp;ref_medium=discover#prizes" rel="noopener noreferrer"&gt;CBIT Hacktoberfest Hackathon 2025&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It was an incredible 24-hour online event organized by the &lt;strong&gt;CBIT Open Source Community&lt;/strong&gt;, celebrating open-source culture and collaboration as part of the global &lt;strong&gt;Hacktoberfest&lt;/strong&gt;. The event gathered hundreds of students from universities across the world — developers, designers, and dreamers ready to learn, code, and share.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Event Was About
&lt;/h2&gt;

&lt;p&gt;Hacktoberfest is all about &lt;strong&gt;celebrating open source&lt;/strong&gt;, and this hackathon perfectly captured that spirit. The CBIT edition — now in its 8th year — encouraged participants to collaborate on innovative solutions using modern technologies while contributing to the open-source ecosystem.&lt;/p&gt;

&lt;p&gt;Teams of &lt;strong&gt;3–5 members&lt;/strong&gt; worked virtually through &lt;strong&gt;Discord&lt;/strong&gt;, tackling real-world challenges in just 24 hours. Despite the distance and time zones, the sense of connection and creativity was palpable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Judging Framework
&lt;/h2&gt;

&lt;p&gt;Every project was evaluated based on a clear, balanced set of criteria (total: 50 points):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Points&lt;/th&gt;
&lt;th&gt;What It Measured&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Innovation &amp;amp; Creativity&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Original ideas and new approaches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Collaboration&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Teamwork, communication, and shared contribution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Implementation&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;Technical soundness, scalability, and efficiency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Usability and user experience&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Presentation&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Clarity, storytelling, and delivery&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As a judge, I was looking for that spark — projects that combined &lt;strong&gt;solid implementation with a clear purpose&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Saw and Learned
&lt;/h2&gt;

&lt;p&gt;Some projects were technically ambitious, pushing the boundaries of what’s possible in 24 hours. Others were beautifully simple, focusing on accessibility or social impact. A few used &lt;strong&gt;AI and automation&lt;/strong&gt; in creative ways that genuinely surprised me.&lt;/p&gt;

&lt;p&gt;But beyond the technology, what stood out was the &lt;strong&gt;teamwork&lt;/strong&gt;. Many participants were strangers before the event — yet they managed to code, design, and present together like long-time collaborators. That’s the magic of hackathons.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Judging Experience
&lt;/h2&gt;

&lt;p&gt;Our panel included engineers and leaders from &lt;strong&gt;Electronic Arts, JP Morgan Chase, Oracle, Deliveroo, ServiceNow, EY&lt;/strong&gt;, and more. Being part of such a diverse group gave every discussion depth. Each judge viewed “innovation” slightly differently — and that variety made our evaluations richer.&lt;/p&gt;

&lt;p&gt;I appreciated the effort teams put into their presentations. Even short demos told full stories — from the problem statement to the final prototype. That’s where creativity met clarity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Innovation isn’t always about technology.&lt;/strong&gt; Sometimes it’s about empathy — understanding who you’re helping and why.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Good presentation matters.&lt;/strong&gt; The best projects told their stories with confidence and focus.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community is everything.&lt;/strong&gt; Open-source hackathons like this show how technology connects us beyond borders.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As a developer, I’ve always believed in continuous learning. Judging this event reaffirmed that growth happens in many forms — whether you’re writing code or evaluating it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Hackathons Matter More Than Ever
&lt;/h2&gt;

&lt;p&gt;In a world where remote collaboration is the new normal, hackathons remain one of the most human ways to innovate. They combine &lt;strong&gt;competition, creativity, and community&lt;/strong&gt; into one shared experience. For students and professionals alike, they’re the perfect place to experiment and grow.&lt;/p&gt;

&lt;p&gt;I’m grateful to have played a small role in this journey — to witness ideas take shape, to learn from participants, and to see firsthand how open source continues to inspire a new generation of builders.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Author:&lt;/strong&gt; &lt;br&gt;
&lt;em&gt;Servin Osmanov&lt;/em&gt;&lt;br&gt;&lt;br&gt;
Lead Software Engineer @ Anvaya Solutions Inc.&lt;br&gt;&lt;br&gt;
Judge at &lt;a href="https://cbit-hacktoberfest25.devpost.com/?ref_feature=challenge&amp;amp;ref_medium=discover#prizes" rel="noopener noreferrer"&gt;CBIT Hacktoberfest Hackathon 2025&lt;/a&gt;&lt;/p&gt;

</description>
      <category>hackathon</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why Language Tech Matters: Developing AI Tools for Small Languages</title>
      <dc:creator>Servin Osmanov</dc:creator>
      <pubDate>Sat, 25 Oct 2025 15:45:27 +0000</pubDate>
      <link>https://dev.to/servin_osmanov/why-language-tech-matters-developing-ai-tools-for-small-languages-583h</link>
      <guid>https://dev.to/servin_osmanov/why-language-tech-matters-developing-ai-tools-for-small-languages-583h</guid>
      <description>&lt;p&gt;In a world where artificial intelligence is transforming how we communicate, the survival of small languages depends not just on cultural passion — but on technology.&lt;br&gt;&lt;br&gt;
While English, Chinese, or Spanish dominate the digital space, thousands of smaller languages remain digitally invisible. Without online presence, data, or digital tools, these languages risk extinction in the 21st century.  &lt;/p&gt;

&lt;p&gt;As a software engineer and cultural advocate, I’ve spent years developing digital tools for the &lt;strong&gt;Crimean Tatar&lt;/strong&gt; language — a Turkic minority language spoken by less than 300,000 people worldwide. Through this work, I’ve learned that the intersection of &lt;strong&gt;AI and linguistics&lt;/strong&gt; isn’t just a research topic; it’s a lifeline for cultural identity.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;The Challenge: The Technology Gap for Small Languages&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Mainstream AI models—from GPT-based chatbots to translation engines—are trained primarily on high-resource languages. This creates a serious imbalance: while global communication becomes easier for some, others are left behind.&lt;/p&gt;

&lt;p&gt;For small linguistic communities, the lack of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;digital corpora,&lt;/li&gt;
&lt;li&gt;standardized spelling systems,&lt;/li&gt;
&lt;li&gt;and high-quality training datasets
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;makes it nearly impossible to integrate their languages into modern tools like voice assistants, translation services, or educational apps.&lt;/p&gt;

&lt;p&gt;When a language isn’t “machine-readable,” it risks becoming irrelevant in the digital world — even if it’s still spoken at home.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Our Journey: Building Tools for the Crimean Tatar Language&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;In 2018, our team launched a series of educational and linguistic tools under the &lt;strong&gt;Qırımtatar Lugatı&lt;/strong&gt; (Crimean Tatar Dictionary) project:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://play.google.com/store/apps/details?id=com.anaurt.lugat" rel="noopener noreferrer"&gt;Android app on Google Play&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apps.apple.com/us/app/q%C4%B1r%C4%B1mtatar-lu%C4%9Fat%C4%B1/id1457493656" rel="noopener noreferrer"&gt;iOS app on the App Store&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our mission was to give people an accessible and modern way to learn, use, and preserve the Crimean Tatar language.  &lt;/p&gt;

&lt;p&gt;The apps quickly grew into more than just dictionaries — they became &lt;strong&gt;living platforms&lt;/strong&gt; connecting speakers, learners, and educators. Yet, users wanted more: context-based search, on-the-fly translation, and natural voice pronunciation.&lt;br&gt;&lt;br&gt;
This inspired us to integrate &lt;strong&gt;artificial intelligence&lt;/strong&gt; to make our tools more flexible and intelligent.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;The Next Step: Bringing AI to Minority Languages&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;We are now integrating &lt;strong&gt;AI and machine learning&lt;/strong&gt; into the Crimean Tatar dictionary project to expand its capabilities:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🧠 &lt;strong&gt;AI-assisted translation:&lt;/strong&gt; dynamic, real-time translation between Crimean Tatar, Turkish, English, and Ukrainian.
&lt;/li&gt;
&lt;li&gt;🔊 &lt;strong&gt;Speech recognition and synthesis:&lt;/strong&gt; allowing users to hear natural pronunciation and practice correct intonation.
&lt;/li&gt;
&lt;li&gt;📖 &lt;strong&gt;Adaptive learning:&lt;/strong&gt; using AI to personalize vocabulary lessons based on user behavior and progress.
&lt;/li&gt;
&lt;li&gt;🪶 &lt;strong&gt;Data-driven NLP foundation:&lt;/strong&gt; building scalable open datasets that will support future chatbots, voice assistants, and translation systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To achieve this, we’ve adopted and fine-tuned &lt;strong&gt;open-source speech synthesis models&lt;/strong&gt; such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://huggingface.co/facebook/mms-tts-crh" rel="noopener noreferrer"&gt;facebook/mms-tts-crh&lt;/a&gt;&lt;/strong&gt; — part of Meta’s &lt;em&gt;Massively Multilingual Speech&lt;/em&gt; project, providing a solid baseline for Crimean Tatar TTS.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://huggingface.co/spaces/robinhad/qirimtatar-tts" rel="noopener noreferrer"&gt;robinhad/qirimtatar-tts&lt;/a&gt;&lt;/strong&gt; — a community-driven model that helps us generate high-quality speech for educational and cultural content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By leveraging these models, we’re building an &lt;strong&gt;AI-powered TTS system&lt;/strong&gt; that brings Crimean Tatar audio resources to learners, teachers, and content creators for the first time.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Why This Matters&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Language is more than communication — it’s collective memory.&lt;br&gt;&lt;br&gt;
When a language disappears, so does a unique worldview and cultural identity.  &lt;/p&gt;

&lt;p&gt;AI gives us a chance to reverse that process. With the right tools, small languages can become visible online, connect their communities, and survive in the digital era.&lt;/p&gt;

&lt;p&gt;Projects like &lt;strong&gt;Qırımtatar Lugatı&lt;/strong&gt; show that you don’t need a giant corporation to make an impact.&lt;br&gt;&lt;br&gt;
A small, passionate team with the right technical vision can give a digital voice — literally — to those who’ve been silent for too long.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;How Developers and Researchers Can Help&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If you’re a developer, linguist, or AI researcher, here are a few ways to make a difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Contribute to &lt;strong&gt;open-source datasets&lt;/strong&gt; for small or low-resource languages.
&lt;/li&gt;
&lt;li&gt;Support &lt;strong&gt;multilingual NLP and TTS&lt;/strong&gt; initiatives on Hugging Face or GitHub.
&lt;/li&gt;
&lt;li&gt;Collaborate with communities and educators who are digitizing endangered languages.
&lt;/li&gt;
&lt;li&gt;Help localize educational and cultural apps into underrepresented languages.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every dataset, every model, and every contribution brings us closer to a more linguistically inclusive AI ecosystem.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Looking Ahead&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Our roadmap for 2025 includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full integration of &lt;strong&gt;AI-powered translation&lt;/strong&gt; within the Qırımtatar Lugatı apps.
&lt;/li&gt;
&lt;li&gt;Implementation of &lt;strong&gt;voice features&lt;/strong&gt; using our fine-tuned TTS models.
&lt;/li&gt;
&lt;li&gt;Launching an &lt;strong&gt;open API&lt;/strong&gt; for developers who want to build tools for Crimean Tatar or similar minority languages.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Technology alone won’t preserve culture — but it can &lt;strong&gt;amplify&lt;/strong&gt; it.&lt;br&gt;&lt;br&gt;
For small languages like Crimean Tatar, &lt;strong&gt;AI isn’t just a tool — it’s hope.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;About the Author&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Servin Osmanov&lt;/em&gt; is a Senior Full-Stack Engineer and founder of the &lt;a href="https://qirim.online/" rel="noopener noreferrer"&gt;&lt;strong&gt;Qırım.Online&lt;/strong&gt;&lt;/a&gt; and &lt;a href="https://ana-yurt.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;Ana-Yurt.Com&lt;/strong&gt;&lt;/a&gt; project — an initiative focused on preserving Crimean Tatar culture and language through modern technology.&lt;br&gt;&lt;br&gt;
LinkedIn: &lt;a href="https://www.linkedin.com/in/servin-osmanov/" rel="noopener noreferrer"&gt;linkedin.com/in/servin-osmanov&lt;/a&gt;&lt;br&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/MrSerWin" rel="noopener noreferrer"&gt;github.com/MrSerWin&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>learning</category>
      <category>programming</category>
      <category>whisper</category>
    </item>
  </channel>
</rss>
