CoreMesh

How to Run Kokoro TTS Locally and Render YouTube Shorts

Install Kokoro TTS on a CPU, pick a voice, and render a 25-second vertical Short with rsvg-convert and ffmpeg, including the music bed and loudnorm.

How to Run Kokoro TTS Locally and Render YouTube Shorts

Kokoro TTS is an 82-million-parameter text-to-speech model, published as Kokoro-82M under the Apache-2.0 licence. It runs on a plain CPU, needs no GPU and no account, and writes a 24 kHz mono waveform you can hand straight to ffmpeg. That's why it's the voice in my YouTube Shorts pipeline.

The pipeline looks like this: the script goes to Kokoro for the voice, SVG frames get rasterised with rsvg-convert, a numpy-generated music bed sits under the voice, and ffmpeg mixes, normalises to -14 LUFS and encodes a 1080x1920, 30 fps, 25-second MP4 of about 1 MB. One render takes about 75 seconds on 4 threads. Every step below comes with a command you can copy.

How to install Kokoro TTS locally#

Kokoro ships as a pip package called kokoro. The weights live on Hugging Face at hexgrad/Kokoro-82M and the code on GitHub at hexgrad/kokoro. The package fetches the model and voice files on first use, so there's no separate download step beyond installing it.

You also need espeak-ng on the system. Kokoro turns text into phonemes with the misaki library, and misaki falls back to espeak-ng for words that aren't in its dictionary. Without it, some sentences error out or come back empty.

bash
# Debian / Ubuntu
sudo apt-get install -y espeak-ng
python3 -m venv .venv && source .venv/bin/activate
pip install kokoro soundfile

On macOS it's brew install espeak-ng followed by the same pip line. On Windows, install the espeak-ng release from its GitHub project first, then run the pip install inside a normal PowerShell venv. If the Windows build fights you, WSL with the Debian commands above is the shortest path.

You don't need a GPU build of torch. The CPU wheel pip picks by default is enough for a 25-second clip. Quick test:

python
from kokoro import KPipeline
import soundfile as sf

pipeline = KPipeline(lang_code='a')  # 'a' = American English, 'b' = British English
for i, (graphemes, phonemes, audio) in enumerate(pipeline('Kokoro is running locally.', voice='af_heart')):
    sf.write(f'test_{i}.wav', audio, 24000)

Play the resulting file. If it sounds like a person, the install is done.

Kokoro TTS voices and how to pick one#

Voice names encode what they are. The first letter is the language, the second is f or m for the speaker's gender, and the rest is a name. The language letters are a for American English, b for British English, j Japanese, z Mandarin, e Spanish, f French, h Hindi, i Italian and p Brazilian Portuguese. The lang_code you give KPipeline has to match the first letter of the voice, otherwise the phonemes don't fit the voice and the output sounds off.

Voice Language Notes
af_heart American English, female The default in most examples
af_bella, af_sarah, af_nicole American English, female Nicole is a whisper-style voice
am_adam, am_michael, am_fenrir American English, male
bf_emma, bf_isabella British English, female
bm_george, bm_lewis British English, male

The full list with quality grades is in the VOICES.md file inside the model repo on Hugging Face. The model page also links a demo Space where you type a sentence and hear it in any voice. That's the fastest way to audition voices before you install anything, and it answers the "kokoro tts voices samples" question better than any written description.

On custom voices: the official repo does not include training code, so there's no supported way to train Kokoro on your own recordings today. What you can do is blend. Each voice pack is a tensor of style vectors, and the pipeline's voice argument accepts a tensor as well as a name. Load two voice files with torch, average them, and pass the result. That gives you a voice between the two, which is as close to "custom" as Kokoro currently gets.

Generating the voice track at the right sample rate#

Kokoro produces audio at 24000 Hz. Write the WAV at that rate, no exceptions. Writing it at 22050 or 44100 is the number one cause of "the voice sounds slowed down" or "the voice sounds like a chipmunk" reports.

The pipeline is a generator. It splits the text (by default on newlines) and yields one audio chunk per piece. Collect every chunk and concatenate before writing, or you'll only get the first sentence.

python
import numpy as np
import soundfile as sf
import torch
from kokoro import KPipeline

torch.set_num_threads(4)

script = open('script.txt').read()
pipeline = KPipeline(lang_code='a')
chunks = [np.asarray(audio) for _, _, audio in pipeline(script, voice='af_heart', speed=1.0)]
voice = np.concatenate(chunks)
sf.write('voice.wav', voice, 24000)
print(round(len(voice) / 24000, 2), 'seconds')

The printed length is the number you plan the video around. If the script runs past 25 seconds, cut words before you touch the speed parameter. Pushing speed much above normal makes the delivery sound rushed, and a Short that feels rushed gets swiped away.

To plan the on-screen text, I split the script into lines and give each line a share of the clip proportional to its word count. This cue sheet is what the SVG frames are built from:

python
# Turn a script into per-line cue times for a 25-second, 30 fps Short
lines = [
    "Kokoro is an 82 million parameter text to speech model.",
    "It runs on a CPU and the weights are Apache licensed.",
    "Here is how a script becomes a vertical video.",
]
total_s, fps = 25.0, 30
words = [len(line.split()) for line in lines]
t = 0.0
for line, w in zip(lines, words):
    dur = total_s * w / sum(words)
    start, end = round(t * fps), round((t + dur) * fps)
    print(f"{t:5.2f}s  frames {start:3d}-{end:3d}  {line}")
    t += dur
print("total frames:", round(total_s * fps))

Rendering the frames with rsvg-convert#

Each scene is an SVG with the caption text, sized 1080x1920. rsvg-convert rasterises it to PNG without a browser, which keeps the whole render on the CPU and free of Chromium.

bash
sudo apt-get install -y librsvg2-bin
mkdir -p frames
for f in svg/*.svg; do
  rsvg-convert -w 1080 -h 1920 -o "frames/$(basename "${f%.svg}").png" "$f"
done

You don't need 750 PNGs for 750 frames. Render one PNG per scene and let ffmpeg's concat demuxer hold each one for its duration. The durations come from the cue sheet above.

text
ffconcat version 1.0
file frames/scene_01.png
duration 8.33
file frames/scene_02.png
duration 8.33
file frames/scene_03.png
duration 8.34
file frames/scene_03.png

The repeated last entry is deliberate. The concat demuxer ignores the duration of the final file unless you list it again, which is documented on the ffmpeg formats page. Keep fonts embedded or installed on the render machine, because rsvg-convert uses system fonts and silently substitutes anything it can't find.

Mixing the music bed and encoding with ffmpeg#

The music bed is a few low sine tones generated in numpy, quiet enough to sit well under speech. It's generated at 24000 Hz on purpose so it matches the Kokoro track and the mixer doesn't have to resample. Here's the same idea in the standard library so you can hear what a generated bed is:

python
import math
import struct
import wave

rate, seconds = 24000, 2
frames = bytearray()
for n in range(rate * seconds):
    t = n / rate
    s = 0.08 * (math.sin(2 * math.pi * 110 * t) + 0.5 * math.sin(2 * math.pi * 165 * t))
    s *= min(1.0, t / 0.5)  # half-second fade in
    frames += struct.pack('<h', int(s * 32767))
with wave.open('bed.wav', 'wb') as w:
    w.setnchannels(1)
    w.setsampwidth(2)
    w.setframerate(rate)
    w.writeframes(bytes(frames))
with wave.open('bed.wav', 'rb') as w:
    print(w.getnframes() / w.getframerate(), 'seconds at', w.getframerate(), 'Hz')

Then one ffmpeg call does the mix, the loudness normalisation and the encode:

bash
ffmpeg -y -threads 4 \
  -f concat -safe 0 -i scenes.txt \
  -i voice.wav -i bed.wav \
  -filter_complex "[1:a][2:a]amix=inputs=2:duration=first[mix];[mix]loudnorm=I=-14:TP=-1.5:LRA=11[a]" \
  -map 0:v -map "[a]" \
  -c:v libx264 -pix_fmt yuv420p -r 30 \
  -c:a aac -b:a 128k -movflags +faststart -shortest short.mp4

What each part does: amix with duration=first ends the mix when the voice ends, loudnorm brings the integrated loudness to -14 LUFS with a -1.5 dBTP ceiling, yuv420p keeps the video playable on phones, and faststart moves the index to the front so playback starts before the download finishes. The filter options are all on the ffmpeg filters page. The -14 LUFS target is the level YouTube normalises loud uploads down to, so hitting it yourself means the platform doesn't change your mix.

The output for a 25-second clip is about 1 MB, and the whole chain from text to MP4 takes about 75 seconds on 4 threads. Kokoro itself is a small part of that; most of the time goes to rasterising and encoding.

Kokoro TTS not working: the usual causes#

These cover almost every "kokoro tts not working" report I've seen, in rough order of likelihood.

  • An espeak or phonemizer error on import or first run. espeak-ng isn't installed, or on Windows the library can't be found. Install it, and on Windows set the PHONEMIZER_ESPEAK_LIBRARY environment variable to the path of the espeak-ng DLL.
  • Only the first sentence is spoken. You saved the first item from the generator and stopped. Collect every chunk and concatenate, as in the script above.
  • The voice is too slow or too fast. The WAV was written at the wrong rate. Kokoro is 24000 Hz.
  • The voice sounds garbled or wrong. The lang_code doesn't match the voice prefix. Use 'a' with af_ and am_ voices, 'b' with bf_ and bm_ voices.
  • Generation is slow. Check that torch isn't fighting other processes for cores. Pinning it to a fixed thread count makes render times predictable.
  • No module named kokoro. The venv isn't activated, or pip installed into a different interpreter than the one running the script.

If your search was actually about Windows Select to Speak or Word's Read Aloud, those are operating system and Office features and have nothing to do with Kokoro. This page won't fix them.

What I'd do differently: check licences before auditioning voices#

I spent time listening to voices from another lightweight CPU engine before looking at their terms. Its best-known English voice, and the fine-tunes built on it, have doubts hanging over commercial use, so I rejected them. The lesson is boring but real: for anything that earns money, read the weights licence and the training-data provenance before you fall in love with a voice.

Kokoro-82M is clean on both counts as far as the official model card goes. The weights are Apache-2.0, and the card states the model was trained on permissive and non-copyrighted audio. That's the reason it got the job, ahead of anything about how it sounds.

Checklist#

  • Install espeak-ng, then pip install kokoro soundfile in a venv.
  • Audition voices in the Hugging Face demo Space, and match lang_code to the voice prefix.
  • Collect every generator chunk, concatenate, and write the WAV at 24000 Hz.
  • Check the printed duration against your 25-second budget and cut words before changing speed.
  • Build one SVG per scene, rasterise with rsvg-convert at 1080x1920, and list scenes in a concat file with the last entry repeated.
  • Generate the music bed at 24000 Hz so it matches the voice.
  • Mix with amix, normalise with loudnorm to -14 LUFS, encode with libx264 and yuv420p.
  • Confirm the licence of every voice you ship, not just the engine.

Quick answers

Where is the Kokoro TTS voices list?

The VOICES.md file in the hexgrad/Kokoro-82M repo on Hugging Face lists every voice with a quality grade. Names follow a pattern: language letter, then f or m, then a name, so af_heart is an American English female voice.

Can I run Kokoro TTS locally without a GPU?

Yes. The CPU build of torch that pip installs by default is enough, and in my pipeline Kokoro is a small share of the roughly 75 seconds a full 25-second Short takes to render on 4 threads.

How do I make a custom voice or train Kokoro TTS?

The official repo doesn't include training code, so there's no supported training path. You can blend two existing voice tensors by averaging them and passing the result as the voice argument.

What sample rate does Kokoro TTS use?

Kokoro outputs 24000 Hz mono. Write your WAV at that rate, and generate any music bed at the same rate so ffmpeg doesn't need to resample.

Why is Kokoro TTS not working on Windows?

Most Windows failures are espeak-ng not being found. Install the espeak-ng release, set PHONEMIZER_ESPEAK_LIBRARY to the DLL path, or use WSL and follow the Linux steps.

Did this solve it?

One tap. No account.

Saved you an hour? Send a tip.

USDC or ETH on Base, Ethereum, Arbitrum and more. Keeps the notes free and the examples running.

Base

Network

0xc783bc5c3fa14cEC3f09A22284bC91C5AF0d59e1
Open in wallet · 5 USDC

On a computer? Scan the code with your phone wallet, or copy the address.

Pick the network your wallet or exchange uses. USDC works on all six; ETH on Base, Ethereum, Arbitrum and Optimism. Don't send on Tron or Solana: this address doesn't exist there. Tips are optional and unlock nothing; every guide stays free.

Discussion

Start the discussion

Indra Wijaya

Builds and runs AI agents, bots and content pipelines, and writes down what works and what broke.

About · Email · RSS

Keep reading

New notes, no spam.

One post at a time, only when there's something worth reading. Follow in any feed reader.

Subscribe via RSS