NoRaincheck

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.

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)

<< Previous Post

|

Next Post >>

🎲 Random post

|

All posts

#Python #Go #ML #ONNX #TTS