Maya1 vs Kokoro vs Kitten TTS Review
Maya1 vs Kokoro vs Kitten TTS Review
March 2026
I finally got to test TTS models, and got them running locally with a variety of notes.
- Kitten: advertised as the smallest model, its also the easiest to setup
- Kokoro: an extremely good model for its size. I found getting it setup with the onnx wrapping to be the most straightforward (supports quants!)
- Maya1: supports gguf. I ended up hosting it in LM Studio with a wrapper to have it working. This is the best quality but also the slowest
TLDR: Use Kokoro if you want a good balance of speed and quality, otherwise Maya1 is a suitable model if you’re willing to wait a bit
Kitten
1from kittentts import KittenTTS
2
3m = KittenTTS("KittenML/kitten-tts-mini-0.8")
4audio = m.generate(text, voice=voice)
Kokoro
1from kokoro_onnx import Kokoro
2
3# https://github.com/thewh1teagle/kokoro-onnx for files (under Releases)
4kokoro = Kokoro("kokoro-v1.0.onnx", "voices-v1.0.bin")
5audio, _ = kokoro.create(text, voice=voice, lang="en-us")
Maya1
Unfortunately I couldn’t get this working with Python’s llama-cpp. Due to the
large size of the model this is mostly unavoidable. Perhaps there is a sensible
way to convert it all to be onnx compatible, that may solve it.
1import httpx
2import torch
3from snac import SNAC
4from transformers import AutoTokenizer
5
6class Maya1Helpers:
7 CODE_START_TOKEN_ID = 128257
8 CODE_END_TOKEN_ID = 128258
9 CODE_TOKEN_OFFSET = 128266
10 SNAC_MIN_ID = 128266
11 SNAC_MAX_ID = 156937
12 SNAC_TOKENS_PER_FRAME = 7
13 SNAC_OFFSET = 4096
14
15 SOH_ID = 128259
16 EOH_ID = 128260
17 SOA_ID = 128261
18 BOS_ID = 128000
19 TEXT_EOT_ID = 128009
20
21 def __init__(self):
22 self.tokenizer = AutoTokenizer.from_pretrained(
23 "maya-research/maya1", trust_remote_code=True
24 )
25 self.snac_model = SNAC.from_pretrained("hubertsiuzdak/snac_24khz").eval()
26
27 def build_prompt(self, text: str, description: str):
28 """Build formatted prompt for Maya1."""
29 soh_token = self.tokenizer.decode([self.SOH_ID])
30 eoh_token = self.tokenizer.decode([self.EOH_ID])
31 soa_token = self.tokenizer.decode([self.SOA_ID])
32 sos_token = self.tokenizer.decode([self.CODE_START_TOKEN_ID])
33 eot_token = self.tokenizer.decode([self.TEXT_EOT_ID])
34 bos_token = self.tokenizer.bos_token
35
36 formatted_text = f'<description="{description}"> {text}'
37
38 prompt = (
39 soh_token + bos_token + formatted_text + eot_token + eoh_token + soa_token + sos_token
40 )
41
42 return prompt
43
44 def _extract_snac_codes(self, token_ids: list) -> list:
45 """Extract SNAC codes from generated tokens."""
46 try:
47 eos_idx = token_ids.index(self.CODE_END_TOKEN_ID)
48 except ValueError:
49 eos_idx = len(token_ids)
50
51 snac_codes = [
52 token_id
53 for token_id in token_ids[:eos_idx]
54 if self.SNAC_MIN_ID <= token_id <= self.SNAC_MAX_ID
55 ]
56
57 return snac_codes
58
59 def _unpack_snac(self, snac_tokens):
60 # Decode SNAC tokens to audio frames
61 frames = len(snac_tokens) // self.SNAC_TOKENS_PER_FRAME
62 codes = [[], [], []]
63 for i in range(frames):
64 s = snac_tokens[i * self.SNAC_TOKENS_PER_FRAME : (i + 1) * self.SNAC_TOKENS_PER_FRAME]
65 codes[0].append((s[0] - self.SNAC_MIN_ID) % self.SNAC_OFFSET)
66 codes[1].extend(
67 [
68 (s[1] - self.SNAC_MIN_ID) % self.SNAC_OFFSET,
69 (s[4] - self.SNAC_MIN_ID) % self.SNAC_OFFSET,
70 ]
71 )
72 codes[2].extend(
73 [
74 (s[2] - self.SNAC_MIN_ID) % self.SNAC_OFFSET,
75 (s[3] - self.SNAC_MIN_ID) % self.SNAC_OFFSET,
76 (s[5] - self.SNAC_MIN_ID) % self.SNAC_OFFSET,
77 (s[6] - self.SNAC_MIN_ID) % self.SNAC_OFFSET,
78 ]
79 )
80 return codes
81
82 def get_audio_from_prompt_output(self, output: str):
83 levels = self._unpack_snac(self._extract_snac_codes(self.tokenizer.encode(output)))
84 codes_tensor = [torch.tensor(level, dtype=torch.long).unsqueeze(0) for level in levels]
85 with torch.inference_mode():
86 z_q = self.snac_model.quantizer.from_codes(codes_tensor)
87 audio = self.snac_model.decoder(z_q)[0, 0].cpu().numpy()
88 return audio
89
90
91API_URL = "http://127.0.0.1:1234/v1/completions"
92HEADERS = {"Content-Type": "application/json"}
93maya1 = Maya1Helpers()
94prompt = maya1.build_prompt(text, description)
95output = httpx.post(
96 API_URL,
97 headers=HEADERS,
98 json={
99 "model": "maya1",
100 "prompt": prompt,
101 },
102 timeout=None,
103)
104output = output.json()["choices"][0]["text"]
105audio = maya1.get_audio_from_prompt_output(output)