<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ai on Chris — freshteapot</title><link>https://freshteapot.net/tags/ai/</link><description>Chris makes things, ships them, and writes down what happened.</description><generator>Hugo 0.163.3</generator><language>en</language><atom:link href="https://freshteapot.net/tags/ai/" rel="self" type="application/rss+xml"/><item><title>Creating word recordings with AI: learning where to cut</title><link>https://freshteapot.net/writing/creating-word-audio-with-ai/</link><pubDate>Tue, 15 Sep 2026 00:00:00 +0000</pubDate><guid>https://freshteapot.net/writing/creating-word-audio-with-ai/</guid><description>A playful spelling game, some troublesome AI audio, and the small tools that helped me turn generated sentences into usable word recordings.</description><content:encoded><![CDATA[<p>I dipped my toe into AI, voice cloning, and generated audio by making a spelling game.</p>
<h2 id="the-game">The game</h2>
<blockquote>
<p>Listen to a word and try to spell it.</p>
</blockquote>
<p>It evolved to include hearts, clues, streaks, single player, multiplayer, replay mode, and all sorts. It&rsquo;s wild what you can do when you have AI at your side.</p>
<p>It was fun to make and fun to play.</p>
<p><a href="game-contact-sheet.webp"><img src="/writing/creating-word-audio-with-ai/game-contact-sheet.webp" alt="A contact sheet of the spelling game, from choosing a Minecraft word list and counting down to entering an answer, using a clue, and finishing the round." width="1408" height="1749" loading="lazy" decoding="async">
</a></p>
<p>The audio was more uneven. Some words sounded great. Others had lost their beginning, stopped too early, or brought a fragment of the preceding word along with them. For a game where the player has to work out what they heard, that is a fairly central problem.</p>
<p>I wanted a WAV file of each word, spoken clearly from beginning to end.</p>
<h2 id="the-voices">The voices</h2>
<p>Using <a href="https://github.com/0xShug0/audio.cpp">audio.cpp</a>, I created AI voices for English, French, and Norwegian.</p>
<p>Initially I used <a href="https://huggingface.co/k2-fsa/OmniVoice">OmniVoice</a>, before moving to <a href="https://github.com/OpenBMB/VoxCPM">VoxCPM2</a> for voice creation. That move was driven by the <a href="#licence">licensing requirements</a>.</p>
<p>I used VoxCPM2 to create a reference recording, then used that recording with <a href="https://huggingface.co/ResembleAI/chatterbox">Chatterbox</a> (model running thru audio.cpp) to generate the sentences. The reference gave Chatterbox an example of the voice and delivery I wanted.</p>
<h3 id="creating-a-voice">Creating a voice</h3>
<p>After my first attempts, a <a href="https://www.reddit.com/r/AskTeachers/comments/1ocvs89/comment/nkpmz1c/">Reddit comment about helping children learn to spell</a> made me think more carefully about the voice. I wanted clear articulation and a patient delivery.</p>
<p>A back-and-forth with AI led to a teacher prompt. Alongside it, I supplied a short reference sentence: enough speech to establish the voice and accent without making the model follow a long passage&rsquo;s rhythm.</p>
<p>Here is a complete command example, using a reference sentence from my experiments. Run it from the audio.cpp directory after building the CLI, and adjust the model path for your setup.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nv">TEACHER_PROMPT</span><span class="o">=</span><span class="k">$(</span>cat <span class="s">&lt;&lt;&#39;PROMPT&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">(
</span></span></span><span class="line"><span class="cl"><span class="s">A professional female Norwegian literacy teacher in her thirties.
</span></span></span><span class="line"><span class="cl"><span class="s">Her voice is warm, calm, clear, and exceptionally articulate.
</span></span></span><span class="line"><span class="cl"><span class="s">She has precise consonants, clean vowels, excellent diction, and natural Norwegian pronunciation.
</span></span></span><span class="line"><span class="cl"><span class="s">Her voice is friendly and reassuring without sounding childish.
</span></span></span><span class="line"><span class="cl"><span class="s">She speaks with a composed, confident teaching presence.
</span></span></span><span class="line"><span class="cl"><span class="s">Her pitch is natural and comfortable.
</span></span></span><span class="line"><span class="cl"><span class="s">Her voice is clean, with no raspiness, breathiness, vocal fry, or exaggerated emotion.
</span></span></span><span class="line"><span class="cl"><span class="s">)
</span></span></span><span class="line"><span class="cl"><span class="s">PROMPT</span>
</span></span><span class="line"><span class="cl"><span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">REFERENCE_TEXT</span><span class="o">=</span><span class="s1">&#39;Når vi snakker tydelig og rolig, blir det lettere å høre hver enkelt lyd. Vi bruker stemmen naturlig, med klare vokaler og presise konsonanter.&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">./build/macos-metal-release/bin/audiocpp_cli <span class="se">\
</span></span></span><span class="line"><span class="cl">  --task tts <span class="se">\
</span></span></span><span class="line"><span class="cl">  --family voxcpm2 <span class="se">\
</span></span></span><span class="line"><span class="cl">  --model <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/models/VoxCPM2-GGUF/voxcpm2-q8_0.gguf&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --backend metal <span class="se">\
</span></span></span><span class="line"><span class="cl">  --text <span class="s2">&#34;</span><span class="si">${</span><span class="nv">TEACHER_PROMPT</span><span class="si">}</span><span class="s2"> </span><span class="si">${</span><span class="nv">REFERENCE_TEXT</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --out teacher.wav
</span></span></code></pre></div><p>I then slowed the recording to 85% of its original tempo:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">ffmpeg -i teacher.wav <span class="se">\
</span></span></span><span class="line"><span class="cl">  -filter:a <span class="s2">&#34;atempo=0.85&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  teacher-slow.wav
</span></span></code></pre></div><p>That gave me a more deliberate reference recording to use for sentence generation. The next job was making the individual words reliable and clear.</p>
<h2 id="the-audio">The audio</h2>
<p>We went on a journey, AI and I. Some experiments moved us forward; others sent us back to listening.</p>
<h3 id="add-knobs">Add knobs</h3>
<p>I started by adjusting the controls available to me: generation steps, guidance, speed, seeds, and word-control tokens.</p>
<blockquote>
<p>Generate a word, listen, change something, try again.</p>
</blockquote>
<p>There were encouraging results. There were also results that made it hard to know what I had learnt. A different seed could rescue a recording, but it did not explain why the previous attempt had failed. A setting that helped one word did not necessarily help the next.</p>
<p>It wasn&rsquo;t going well.</p>
<h3 id="give-the-word-a-sentence">Give the word a sentence</h3>
<p>The models I tried struggled with isolated words, so we added a sentence template to give them more context and encourage a natural delivery.</p>
<p>This gave the model some context and gave me a pattern to look for. The last word should follow a pause. Find that pause, cut there, and save the result.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">This is the word ‘acquaintance’. Acquaintance.
</span></span></code></pre></div><h3 id="the-problem">The problem</h3>
<p>Where should I cut the sentence to capture the final word?</p>
<p>It sounded like something FFmpeg could do in one line. That turned out to be wishful thinking.</p>
<p>The first useful distinction was between detecting silence and deciding what that silence meant. FFmpeg could report quiet intervals. It could not tell me that a particular interval separated the introductory sentence from the final word.</p>
<p>Even reading the settings involved some learning. A minimum silence of <code>0.12</code> meant a quiet interval had to last 120 milliseconds to qualify. It was not how long to record. If the useful pause was shorter, my script could miss it completely.</p>
<p>Reducing that threshold found more pauses. It also gave the script more opportunities to pick the wrong one.</p>
<h3 id="a-plausible-cut-can-still-be-wrong">A plausible cut can still be wrong</h3>
<p>I tried rules based on the last pause, the longest pause, and how much sound remained afterwards.</p>
<p>Each had a reason behind it. Each met a recording that exposed the assumption.</p>
<p>With <code>kjole</code>, an early cut kept almost the whole sentence. Rejecting an output that retained 80% or more of the recording caught that sort of mistake. But a file could be much shorter than the original and still contain the wrong audio.</p>
<p>At the other extreme, one candidate left only about 26 milliseconds of sound above the threshold. That was a useful clue: perhaps I had found a gap inside the word and kept only its ending.</p>
<p>I added a rough minimum based on the number of letters. That helped reject implausibly small fragments, but letters are an imperfect measure of spoken duration. Quiet sounds also complicate any rule based on time above a volume threshold.</p>
<p><code>Acquaintance</code> kept coming back. Its ending sounded troublesome even when I could hear the sentence pronounce it properly. But a check of the trial clip showed it contained the complete, unchanged ending. My first description of the problem was not enough to diagnose it. <code>Jente</code> exposed a different difficulty: the gap before the repeated Norwegian word could be tight enough that a seemingly reasonable cut still sounded wrong.</p>
<p>I needed to see what the rules were choosing.</p>
<h3 id="visualise">Visualise</h3>
<p>I asked AI to build a small browser tool for opening a WAV, selecting a region, and playing exactly that region. It became audio-lab. As it evolved, it became much easier to show the AI what I meant and check the changes we made.</p>
<p>I could drag the boundaries, zoom in, slow playback, and listen again. Then we added silence detection, with all the detected gaps highlighted together over the waveform.</p>
<p>The decibel threshold made more sense when I could change it and see which areas qualified as quiet. These bands represented sound below a chosen threshold for a chosen duration. They were not proof of empty space between words. A quiet part of speech could end up inside a band too.</p>
<p>The next useful addition was selection JSON: a small record of the file and the start and end times of a region. The extractor could write its proposed selection, and audio-lab could load it over the original recording.</p>
<p>That made the script&rsquo;s decision inspectable. I could load its JSON, hear exactly what it proposed, and compare the boundary with the detected gaps. A file fingerprint helped make sure the selection belonged to the recording I had opened.</p>
<p>I could also move the selection by hand and copy the new coordinates back into the discussion. Instead of describing a vague problem at the beginning, I could point to an interval and explain what I heard there.</p>
<h3 id="hear-the-difference">Hear the difference</h3>
<p>Here&rsquo;s what those decisions looked and sounded like. This recreated comparison uses one saved <code>acquaintance</code> sentence, made with a later template. Both cuts use that same recording, so the comparison demonstrates the cutting failure, not a change in generated speech.</p>
<figure style="margin:2rem 0;padding:1rem;border:1px solid currentColor;border-radius:0.75rem">
  <figcaption style="margin-bottom:0.75rem"><strong>The complete sentence</strong><br>Listen for the final repetition of acquaintance.</figcaption><audio controls preload="metadata" aria-label="The complete sentence" style="display:block;width:100%;min-width:0">
    <source src="/writing/creating-word-audio-with-ai/acquaintance-sentence.wav" type="audio/wav">
    Your browser does not support embedded audio. Use the download link below.
  </audio>
  <p style="margin:0.75rem 0 0;font-size:0.9em"><a href="/writing/creating-word-audio-with-ai/acquaintance-sentence.wav" download>Download WAV</a></p>
</figure>
<p>The simple rule chooses a short gap after the final word has begun. The later rule chooses the longer gap before it. Both keep the original ending.</p>
<figure style="margin:2rem 0;padding:1rem;border:1px solid currentColor;border-radius:0.75rem">
  <figcaption style="margin-bottom:0.75rem"><strong>Bad cut: the opening is missing</strong><br>Recreated cut: 2.755–3.760 seconds. Notice the small burst immediately to the left of the green selection.</figcaption><a href="/writing/creating-word-audio-with-ai/acquaintance-bad.png" aria-label="Open full-size screenshot: Bad cut: the opening is missing"><img src="/writing/creating-word-audio-with-ai/acquaintance-bad.png" width="2416" height="1762" alt="Audio-lab shows the green selection starting at 2.755 seconds, just after the final word&#39;s opening burst. Amber bands show detected quiet gaps." loading="lazy" style="display:block;width:100%;height:auto;margin:0 0 1rem"></a><audio controls preload="metadata" aria-label="Bad cut: the opening is missing" style="display:block;width:100%;min-width:0">
    <source src="/writing/creating-word-audio-with-ai/acquaintance-bad.wav" type="audio/wav">
    Your browser does not support embedded audio. Use the download link below.
  </audio>
  <p style="margin:0.75rem 0 0;font-size:0.9em"><a href="/writing/creating-word-audio-with-ai/acquaintance-bad.wav" download>Download WAV</a> · <a href="/writing/creating-word-audio-with-ai/acquaintance-bad.selection.json" download>Download selection JSON</a></p>
</figure>
<figure style="margin:2rem 0;padding:1rem;border:1px solid currentColor;border-radius:0.75rem">
  <figcaption style="margin-bottom:0.75rem"><strong>Corrected cut: the opening is retained</strong><br>Corrected cut: 2.489–3.760 seconds. The selection now includes that opening. Both screenshots show silence at −35 dB for at least 60 ms.</figcaption><a href="/writing/creating-word-audio-with-ai/acquaintance-good.png" aria-label="Open full-size screenshot: Corrected cut: the opening is retained"><img src="/writing/creating-word-audio-with-ai/acquaintance-good.png" width="2416" height="1762" alt="The same sentence in audio-lab with the green selection starting earlier, at 2.489 seconds, in the gap before the final word." loading="lazy" style="display:block;width:100%;height:auto;margin:0 0 1rem"></a><audio controls preload="metadata" aria-label="Corrected cut: the opening is retained" style="display:block;width:100%;min-width:0">
    <source src="/writing/creating-word-audio-with-ai/acquaintance-good.wav" type="audio/wav">
    Your browser does not support embedded audio. Use the download link below.
  </audio>
  <p style="margin:0.75rem 0 0;font-size:0.9em"><a href="/writing/creating-word-audio-with-ai/acquaintance-good.wav" download>Download WAV</a> · <a href="/writing/creating-word-audio-with-ai/acquaintance-good.selection.json" download>Download selection JSON</a></p>
</figure>
<p>The controls below each screenshot play its extracted WAV; click the image to inspect it at full size. To reproduce the selections in audio-lab, open the complete sentence WAV and then load either selection JSON. The <a href="reproduce-examples.txt">reproduction commands</a> include the extraction settings.</p>
<p>This was a particularly useful role for AI. It could make the interface I needed while I was still discovering what I needed to inspect.</p>
<h3 id="estimate-the-sentence-then-look-nearby">Estimate the sentence, then look nearby</h3>
<p>We knew more than the waveform alone. We knew the word and the sentence template.</p>
<p>That suggested estimating where the introductory sentence should end, then looking for a nearby pause. I tried counting letters and assigning a rough number of milliseconds per letter.</p>
<p>There was a mistake hidden in that too. The small number I had used as a minimum-duration check for a word was unsuitable as an estimate of normal speaking time. Those calculations served different purposes, even though both used milliseconds per letter.</p>
<p>A larger estimate got us into a more useful part of the recording. Then leading silence, speaking pace, and trailing silence became important. A fixed rate was still a guess, and different voices did not all speak at that rate.</p>
<p>The next step used the recording itself to estimate the rate. Find the sentence&rsquo;s start and the last sound, account for the known text, and evaluate candidate gaps with their duration taken out of the timing calculation. Use that estimate to help choose a plausible boundary.</p>
<p>This was still a collection of heuristics. I was learning from examples and improving rules; I was not training a model to recognise word boundaries.</p>
<p>Audio-lab made that distinction useful rather than frustrating. I could inspect a guess and work out why it had failed.</p>
<h3 id="change-the-sentence-too">Change the sentence, too</h3>
<p>After all that attention to the cut, I learnt something I wish I had tried earlier: changing the sentence template could improve the generated audio itself.</p>
<p>I experimented with the wording, quotation marks, and punctuation:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">This is the word ‘{word}’. {spoken}.
</span></span><span class="line"><span class="cl">This is the word &#34;{word}&#34;. {spoken}.
</span></span><span class="line"><span class="cl">This is the word &#39;{word}&#39;. {spoken}.
</span></span><span class="line"><span class="cl">This is the word &#39;{word}.&#39; {spoken}.
</span></span></code></pre></div><p>This version worked best in my English tests:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Please say the word &#39;{word}.&#39; {spoken}.
</span></span><span class="line"><span class="cl">Please say the word &#39;acquaintance.&#39; Acquaintance.
</span></span></code></pre></div><p>Notice the full stop inside the quotation marks and the capitalised first letter of the repeated word. The combination helped in my tests. I hadn&rsquo;t isolated each change, so I couldn&rsquo;t say how much the wording, punctuation, or capitalisation contributed individually.</p>
<p><code>Acquaintance</code> worked cleanly with this template. The source sentence in the audio comparison above uses it. I had been asking where to cut the recording; changing what I asked the model to say was another way to improve the result.</p>
<h3 id="give-cleanup-its-own-job">Give cleanup its own job</h3>
<p>A later <code>acquaintance</code> recording had a little hiss before the word. The main extraction could get me close while still leaving an unwanted opening sound.</p>
<p>I could have kept adding conditions to the extractor. Instead, I added a separate cleanup step operating on the extracted word.</p>
<p>It examined the beginning for an opening sound followed by a qualifying quiet gap, then proposed a later start with a little padding. It wrote a preview WAV and JSON so I could inspect the result in audio-lab before accepting it.</p>
<p>This was trimming an unwanted lead-in, rather than removing noise throughout the recording. Finding a quiet gap did not by itself establish that everything before it was disposable.</p>
<p>Separating the steps helped. I could keep the generated sentence, repeat extraction, and try cleanup settings independently. Some recordings that still sounded rough after extraction became useful after cleanup.</p>
<h3 id="a-second-opinion-and-a-better-listening-desk">A second opinion, and a better listening desk</h3>
<p>I used <a href="https://github.com/openai/whisper">Whisper</a> and <a href="https://huggingface.co/NbAiLab/nb-whisper-small">nb-whisper</a> to compare recognised text with the expected word. A mismatch gave me somewhere to look.</p>
<p>There was a configuration mistake here too: sending <code>language=no</code> did not mean I was using nb-whisper. It selected Norwegian transcription on the server receiving the request. The regular Whisper and nb-whisper models were running separately; I had to use the correct endpoint.</p>
<p>Recognition results also needed listening judgment. In one Norwegian batch, seven recordings were flagged, but after listening I considered only one wrong. In another English check, I agreed with the seven flagged results. A mismatch was useful evidence, not a verdict.</p>
<p><code>review.py</code> made this manageable. It turned the batch JSON into a listening page, with the source sentence, extracted audio, available cleanup previews, word IDs, transcripts, and expandable metadata. Filters let me concentrate on mismatches or recordings with cleanup versions.</p>
<p><img src="/writing/creating-word-audio-with-ai/review-acquaintance.png" alt="The review page filtered to acquaintance, showing its sentence, extracted audio, word ID, and Whisper match." title="One search brings the sentence, extracted word, and recognition result together. No cleanup clip was proposed for this particular saved take." width="2272" height="1122" loading="lazy" decoding="async">
</p>
<p>That little page saved a lot of opening files and remembering which version I had just heard.</p>
<h2 id="licence">Licence</h2>
<p>I wanted to keep the possibility of commercial use open, and the specific OmniVoice Word-Control weights I was trying were marked CC-BY-NC-4.0. Checking the licence on the code alone would have missed that. <a href="https://huggingface.co/multimodalart/omnivoice-word-control#license--lineage">OmniVoice Word-Control model card</a>.</p>
<p>That pushed me towards Chatterbox for the sentence-generation workflow and VoxCPM2 for voice experiments. Their published model cards list MIT and Apache-2.0 respectively. The particular model and weights mattered when making that decision. <a href="https://huggingface.co/ResembleAI/chatterbox">Chatterbox</a>, <a href="https://huggingface.co/openbmb/VoxCPM2">VoxCPM2</a>.</p>
<h2 id="tooling">Tooling</h2>
<p>The two small tools made a big difference: audio-lab let me inspect a cut, and the review page let me move quickly through a batch, listening and comparing versions.</p>
<p>Before AI, I would have weighed up the time needed to build a tool against the time it might save. That was harder when I didn&rsquo;t yet know what the tool needed to do. With AI at my side, I could get a first version in minutes, then improve it in small steps as I learnt.</p>
<p>I still had to listen, notice the mistakes, and decide what was worth trying next. But I could turn a question into a waveform selection, a comparison page, or a repeatable command while the problem was fresh in my mind. Without those tools, I don&rsquo;t think I would have reached my virtual finish line.</p>
<p>The goal stayed simple: press play and hear one complete word, clearly enough to have a fair go at spelling it.</p>
<h2 id="references">References</h2>
<h3 id="projects-and-models">Projects and models</h3>
<ul>
<li><a href="https://github.com/0xShug0/audio.cpp">audio.cpp</a></li>
<li><a href="https://huggingface.co/k2-fsa/OmniVoice">OmniVoice model card</a></li>
<li><a href="https://huggingface.co/multimodalart/omnivoice-word-control#license--lineage">OmniVoice Word-Control model card and licence</a></li>
<li><a href="https://github.com/OpenBMB/VoxCPM">VoxCPM source code</a></li>
<li><a href="https://huggingface.co/openbmb/VoxCPM2">VoxCPM2 model card</a></li>
<li><a href="https://huggingface.co/ResembleAI/chatterbox">Chatterbox model card</a></li>
<li><a href="https://github.com/openai/whisper">Whisper source code and documentation</a></li>
<li><a href="https://huggingface.co/NbAiLab/nb-whisper-small">nb-whisper-small model card</a></li>
</ul>
<h3 id="reading-and-notes">Reading and notes</h3>
<ul>
<li><a href="https://www.reddit.com/r/AskTeachers/comments/1ocvs89/comment/nkpmz1c/">Reddit comment about helping children learn to spell</a></li>
<li><a href="#licence">Licensing notes in this article</a></li>
</ul>
<h3 id="examples-and-downloads">Examples and downloads</h3>
<ul>
<li><a href="game-contact-sheet.webp">Game contact sheet</a></li>
<li><a href="acquaintance-sentence.wav">Complete acquaintance sentence (WAV)</a></li>
<li>Recreated bad cut: <a href="acquaintance-bad.wav">WAV</a>, <a href="acquaintance-bad.selection.json">selection JSON</a>, <a href="acquaintance-bad.png">audio-lab screenshot</a></li>
<li>Corrected cut: <a href="acquaintance-good.wav">WAV</a>, <a href="acquaintance-good.selection.json">selection JSON</a>, <a href="acquaintance-good.png">audio-lab screenshot</a></li>
<li><a href="review-acquaintance.png">Review-page screenshot</a></li>
<li><a href="reproduce-examples.txt">Reproduction commands</a></li>
</ul>
]]></content:encoded></item></channel></rss>