Kokoro-82M โ€” ExecuTorch (text to speech, 54 voices)

Phonemes in, a waveform out, in one .pte with two methods. StyleTTS2-shaped: a 12-layer phoneme BERT and a duration predictor decide how long each sound lasts, and an iSTFTNet vocoder turns the stretched features into 24 kHz audio. A single 256-dimensional style vector picks the voice and colours the prosody.

predict  input_ids (1, N) int64, ref_s (1, 256) fp32, speed (1) fp32
             ->  d (1, 640, N), t_en (1, 512, N), duration (N) int64
vocode   d, t_en, aln (1, N, F) fp32, ref_s
             ->  waveform (F * 600) fp32 @ 24 kHz
  • File: kokoro_82m_xnnpack_fp32.pte โ€” 325.4 MB, two methods
  • Source: hexgrad/Kokoro-82M โ€” 81.8M parameters
  • License: apache-2.0
  • Voices: 54, shipped separately as voices/*.pt in the source repo
  • Languages: the graph is language-blind โ€” it takes phoneme ids. Verified here in English and Japanese; the voice pack also carries Spanish, French, Hindi, Italian, Portuguese and Chinese voices, which are not verified on this card.

Both axes โ€” phonemes and frames โ€” are dynamic, and both are exact. Nothing is padded, nothing is stretched, and there is no ladder of fixed-size methods. That took some doing; see below.

Running it

1. Text to phonemes, outside the graph. Kokoro's vocabulary is IPA, and getting there is misaki (or espeak-ng) โ€” a lexicon and a G2P model, not arithmetic. Same line this shelf takes with E5's prefix and Whisper's mel: the recipe is here, the graph takes what the recipe produces.

from misaki import en
ps, _ = en.G2P(trf=False, british=False)(text)
ids = [vocab[c] for c in ps if c in vocab]          # config.json's 178-entry vocab
input_ids = torch.LongTensor([[0, *ids, 0]])        # wrapped in the boundary token

For Japanese it is misaki.ja.JAG2P() and a j* voice, and nothing else changes โ€” the ids go into the same graph. Install misaki[ja] with unidic-lite: unidic's dictionary is a separate 250 MB download, and fugashi prefers unidic when both are present, which leaves you with an empty dicdir and an error about mecabrc.

2. Pick the style row by phoneme count. The voice pack is 510 rows and the row is pack[len(ps) - 1] โ€” indexed by the length of the phoneme string, which is what upstream's own pipeline does, and not by the number of ids, which is smaller whenever the string holds a character the vocabulary does not have.

3. Run predict, which returns the features and one duration per phoneme.

4. Build the alignment matrix yourself, at exactly sum(duration) frames. Upstream builds it with repeat_interleave, whose width is the sum of the durations โ€” the model's own output deciding the shape of its next input, which no single graph can express. That is the only reason this is two methods rather than one. The same matrix is comparisons only:

ends = torch.cumsum(duration, 0)
starts = ends - duration
frame = torch.arange(int(ends[-1]))                 # exactly sum(duration)
aln = ((frame[None, :] >= starts[:, None]) &
       (frame[None, :] < ends[:, None])).float()[None]

Give it exactly sum(duration) frames. Not more โ€” see the next section.

5. Run vocode. Out comes 600 * F samples at 24 kHz. speed above 1 speaks faster; it divides the durations before they are rounded.

Do not pad either axis

Both axes are dynamic, so there is no window to pad into โ€” but it is worth saying why the file is built that way, because the obvious fixed-window design does not work here and the damage does not show up in a transcript.

axis what forbids padding measured
phonemes five bidirectional LSTMs โ€” state flows in from the padding speaking rate moves up to 19%
frames a bidirectional LSTM and AdaIN1d, which is InstanceNorm over time log-mel 0.18โ€“0.86 against a 0.04 noise floor

The frame axis is the surprising one. AdaIN1d normalises over time, so one extra frame changes the statistics the entire signal is divided by. Padding to the next 16-frame rung, appending 256 frames, and padding out to 1024 all land far outside what the model does to itself, and it is not a level change โ€” taking out one global gain factor leaves the distance where it was.

Padding with spaces rather than zeros roughly halves the damage on the phoneme axis, and a recogniser transcribes every padded arm correctly. That is exactly why the gate here is not a recogniser alone.

The LSTMs are rolled, not unrolled

nn.LSTM will not export with a dynamic sequence axis: torch.export pins it to whatever it was traced at. The reason is that to_edge unrolls the recurrence โ€” this model's predict graph is 1238 ATen nodes at any length, and 1651 + 108 per phoneme in edge dialect.

That makes a ladder of fixed-length methods look like the only option, and then makes the ladder impossible. The XNNPACK partitioner is superlinear in node count and cuts an unrolled LSTM into hundreds of tiny delegates โ€” 383 partitions at 32 phonemes โ€” so lowering one method costs:

phonemes edge nodes lowering
8 1651 33 s
16 2515 63 s
32 4243 153 s
128 14611 ~19 min, extrapolated

A rung per phoneme count from 8 to 128 is upwards of 16 hours, and the frame axis would need its own ladder on top of that.

A scan higher-order op keeps the loop rolled. ExecuTorch lowers it, the runtime runs it, and the sequence axis stays dynamic. On this model's own LSTM shape โ€” 640 in, 256 hidden each way, 128 steps:

build edge nodes delegates 128 steps
nn.LSTM, one fixed length 59.2 s 3366 131 5.72 ms
rolled scan, any length 3.1 s 49 3 16.65 ms

Three times the runtime for one LSTM, against a build that finishes and a file that takes any length. Kokoro has six of them. The whole file now builds in about two minutes.

Verification

Ten sentences through misaki โ€” five English on af_heart, five Japanese on jf_alpha โ€” each one synthesised by the .pte and by the unmodified upstream model in eager. Three gates, because no single one works here:

Durations must match exactly โ€” they are integers, they decide the rhythm, and they come out of the half of the model that has no noise in it. 10 of 10 exact, both languages.

Waveform correlation is not usable. The vocoder's excitation carries Gaussian noise and a random initial phase, so the eager model does not reproduce itself: two runs of the same input correlate 0.9948. A correlation gate here measures the noise.

Log-mel distance against the model's own floor is the gate that works. Measure the distance between two eager runs, then between eager and the .pte, and ask whether the second is the first:

log-mel vs eager eager's own floor ratio
worst of ten 0.0474 0.0455 1.04x
best of ten 0.0393 0.0408 0.96x

Below and above 1.0 across the five, which is what "indistinguishable from running it again" looks like โ€” and it moves a few percent between runs, because the noise source is in both arms. For scale, one extra frame of padding shows up at 4.4x, and the fp16 build below at 73x.

Transcripts, through Qwen3-ASR, scored against eager's own transcript rather than against the sentence โ€” what is under test is the conversion, and a recogniser choosing a different kanji is not the file's doing. CER 0.0000 on nine of ten.

The tenth is worth writing down, because it is the recogniser and not the model:

ใ“ใฎ้›ป่ปŠใฏๆฑไบฌ้ง…ใซๆญขใพใ‚Šใพใ™ใ‹   (does this train stop at Tokyo Station)
eager  ...ๆฑไบฌ้ง…ใซๆณŠใพใ‚Šใพใ™ใ‹    (stay overnight)      CER 0.067
pte    ...ๆฑไบฌ้ง…ใซๅœใพใ‚Šใพใ™ใ‹    (stop)

Same reading, different kanji โ€” and eager disagrees with itself here: four runs of the identical input gave ๆณŠ once and ๅœ three times, and the .pte did the same. The vocoder's noise is enough to tip a near-tie in the recogniser. Durations are identical and log-mel is 1.02x the floor on this clip, so the two files are as close as eager is to itself. An ASR-only gate would have recorded this as a defect.

Speed

3.25 s of audio (50 phonemes, 130 frames) on an M-series laptop, host CPU:

ms
predict 56.6
vocode 441.4
total 498.0 โ€” 6.5x faster than real time
the same utterance in eager PyTorch 225.5

Measured on a machine that was busy, so read these as a floor rather than a number to quote. Eager is faster here and that is expected: it reaches Accelerate's LSTM and convolution kernels, while about half the graph runs on portable kernels (predict 49.6% of ops delegated, vocode 54.8%). No device numbers yet.

What is not in this file

fp16 was built and withdrawn. It fails three ways at once, and the first one is not about ExecuTorch at all:

fp32 (this file) fp16 (withdrawn)
size 325.4 MB 278.7 MB โ€” a 14% saving, not the 50% the weights imply
worst log-mel / noise floor 0.99x 73x
worst CER 0.0000 1.0000 โ€” nothing intelligible
3.25 s of audio 498 ms 2104 ms

Halving this model breaks it in eager PyTorch, before any export. At 96 phonemes the vocoder returns nan; at 24 it survives but sits at 2.8x the noise floor. Keeping the 73 data-statistic norm layers in fp32 โ€” the usual fix for InstanceNorm overflow โ€” does not save it, so the overflow is not only in the norms. One duration in 96 also flips, which is a rounding tie rather than a numerical failure.

The size is the least of it but worth knowing: the weights do halve to 163.4 MB, and the file is still 278.7 MB, because the XNNPACK delegate carries its own copy of the weights it takes.

No int8 build. Measured, not assumed: dynamic int8 quantises nn.Linear only, and Kokoro is 66.9% Conv1d with just 15.9% of its weights in Linear layers, so it would touch a sixth of the file. Static int8 would reach the convolutions, but a vocoder's quality under quantisation has to be measured per task rather than declared, and that has not been done here.

The vocoder is partitioned without XNNPACK's PermuteConfig. A permute inside the decoder feeds two consumers outside its partition, and the delegate's output list then carries that one node twice, which XNNPACK rejects with "Output node ... is already in the inputs ... pass through arguments". It is the partition boundary that is wrong, not the graph: the same graph lowers the moment permutes are not eligible to be one.

Downloads last month
3
Inference Providers NEW
This model isn't deployed by any Inference Provider. ๐Ÿ™‹ Ask for provider support

Model tree for mlboydaisuke/Kokoro-82M-ExecuTorch

Quantized
(62)
this model