Kokoro-82M
hexgrad's Kokoro-82M TTS model, exported for loom.cpp. Takes phoneme ids, not text.
This is a loom.cpp export: a single self-describing GGUF that carries its own graph topologies, tokenizer (if any) and driver script, produced by loom-exporter.
Original model
Exported from hexgrad/Kokoro-82M. Weights are unmodified; this repo packages the same parameters into
loom.cpp's GGUF format.
License
apache-2.0, inherited from the base model above.
Language(s)
en
upstream's own language: tag is en; the model card additionally documents 8 languages / 54 voices
Usage
Run it with loom-py -- loom-py-rt on PyPI:
pip install -U "loom-py-rt[hub,phonemes]"
NOTE: This is a work in progress. For now, in order to avoid license conflicts and keep
dependencies at a minimum, we opted for orthography2ipa as our "swiss-knife" phonemizer. For
deep-orthography languages like English, to get stressing rules and context-based phonemization, the
phonemizer must register a reference lexicon (e.g., ipa-dict) to get highly accurate phonemization.
Full quality can be achieved by phonemizing the text yourself using your engine of choice and feeding
it to the model via the argument phonemes as in the example below.
import loom
model = loom.Model.from_pretrained("loom-ai-org/kokoro-82m-loom")
# kokoro-82m is trained on phonemes. Its symbol table ships in the GGUF, so the only piece that is not in
# the file is grapheme-to-phoneme -- a property of the language rather than of this checkpoint, which
# is why it is the `phonemes` extra above rather than part of the model.
# THE FULL-QUALITY PATH: phonemes you produced yourself, with whatever G2P you trust. The symbol table
# in the GGUF is what encodes them, so anything that emits IPA works.
audio = model.text2speech.infer(phonemes="həˈloʊ wˈɜːld", sample_rate=24000)
audio.save("out.wav")
# THE BUILT-IN PATH: text straight in, phonemized by the bundled rule-based G2P. Good enough for
# shallow orthographies; for English see the note above, and give it a lexicon so it has stress and
# real vowels to work with -- "time" is /tɪm/ without one.
#
# open-dict-data/ipa-dict (MIT) publishes ~65k-entry wordlists WITH stress for en_UK and en_US, in
# almost the right shape: its IPA is wrapped in slashes and a rare entry carries two comma-separated
# variants, both of which the loader rejects. Take the RAW file: a github.com/.../blob/... URL serves
# an HTML PAGE, and the sed below will turn that into a .tsv that parses to zero entries -- which is
# indistinguishable from no lexicon at all except for the warning `set_lexicon` raises. Two lines:
# curl -LO https://raw.githubusercontent.com/open-dict-data/ipa-dict/master/data/en_UK.txt
# sed 's:/::g; s/\t\([^,]*\),.*/\t\1/' en_UK.txt > en_UK.tsv
loom.phonemizers.set_lexicon("en_UK.tsv") # a path, an http(s):// URL, or hf://<repo>/<path>
# sample_rate=24000: this checkpoint does not carry its own rate, so it is a value you have to
# know from the model's documentation and pass. It is used only if the GGUF declares none; a wrong rate
# does not fail, it plays the voice at the wrong speed.
audio = model.text2speech.infer("hello world", sample_rate=24000)
audio.save("out.wav")
Choosing a voice
This file embeds one voice (af_heart) and uses it whenever no style is passed. All 54 upstream voice
packs ship in this repo under voices/, copied unmodified:
import torch
from huggingface_hub import hf_hub_download
path = hf_hub_download("loom-ai-org/kokoro-82m-loom", filename="voices/af_bella.pt")
pack = torch.load(path, weights_only=True) # (510, 1, 256) float32
# A Kokoro voice is a PACK, not a vector: one 256-float style per phoneme count, and upstream picks the
# row by the length of the phoneme string (`pack[len(ps)-1]`). That is why the phonemes are produced
# here rather than inside `infer` -- the row and the ids come from the same string.
phonemes = loom.phonemizers.phonemize("hello world")
ref_s = pack[len(phonemes) - 1].flatten().tolist() # 256 floats
# A specific voice is a knob the high-level door does not name, so it rides through as a driver input.
audio = model.text2speech.infer(tokens=model.tokenize(phonemes), ref_s=ref_s)
audio.save("out.wav")
ref_s is the two 128-float halves back to back -- decoder style first, predictor style second, which is
the order KModel.forward splits it in -- so a row of an upstream pack is already in the right layout
and needs no rearranging. A different voice predicts different durations, so the waveform generally
changes length as well as timbre.
torch appears here only to read upstream's .pt format; loom-py itself has no runtime dependencies
and takes any sequence of 256 floats, so a voice you have stored some other way works the same way.
Which voice is which is upstream's own VOICES.md, copied into this repo: 8 languages, with a quality
and training-duration grade per voice. Note that the phonemes have to match the voice's language, and
the G2P above is the one you installed -- a Japanese voice needs Japanese phonemes to sound like
anything.
The layer underneath
The call above is the high-level door: one per task, named for the modality pair it maps between, with
the windowing, sampling and assembly this model needs already applied. Under it, model.infer(...)
passes your arguments straight to the driver this GGUF embeds -- which is where you go for a knob the
door does not name.
model.driver_source prints that driver, including a header comment documenting every argument it
accepts for this model, and is the authority on it. See loom-py for the API and
loom.cpp for what the engine does between the two.
Files
kokoro-82m.gguf-- the model, exported with loom-exporter.voices/*.pt-- all 54 upstream voice packs, copied unmodified fromhexgrad/Kokoro-82M.af_heartis also embedded in the GGUF as the default, so these are only needed to select a different voice. See the usage example above.VOICES.md-- upstream's own voice table (language, traits, quality grades), copied unmodified.