kokoro-ru
Russian text-to-speech at **82M parameters**, running **9.8x faster than realtime on a laptop CPU** with no GPU. Three fixed voices from consented studio actors. A Russian port of [Kokoro-82M](https://huggingface.co/hexgrad/Kokoro-82M). RTF 0.102 9.8x realtime, CPU only params 81.81M peak RAM 2.39 GB voice gender checkpoint sveta female kokoro-ru-v2-base.pth masha female kokoro-ru-v2-base.pth dima male kokoro-ru-v2-dima.pth Sveta — heard in the clip above — is the flagship voice. She and Masha share one checkpoint and differ only by voicepack, so the release is two model files, not three. Works with the stock `kokoro` package, unmodified. import torch, soundfile as sf from kokoro import KModel from huggingface_hub import hf_hub_download from ru_g2p import RuG2P # this repo REPO = "zaakirio/kokoro-ru" g2p = RuG2P() model = KModel(repo_id=REPO, model=hf_hub_download(REPO, "kokoro-ru-v2-base.pth")).eval() pack = torch.load(hf_hub_download(REPO, "voices/sveta.pt"), map_location="cpu", weights_only=False) ipa, oov = g2p("Здравствуйте! Как ваши дела?") assert not oov with torch.no_grad(): audio = model(ipa, pack[len(ipa) - 1], 1.0, return_output=True).audio sf.write("out.wav", audio.cpu().numpy(), 24000) Russian TTS lives or dies on lexical stress — за́мок is a castle, замо́к is a padlock. `ru_g2p.py` resolves stress, ё and homographs with [RUAccent](https://github.com/Den4ikAI/ruaccent), applies Russian vowel reduction, then phonemizes. **Feed it text, not IPA.** Whisper large-v3 round-trip against [Piper](https://github.com/rhasspy/piper), the incumbent free local Russian TTS, on identical sentences with identical ASR and normalisation. We average **2.50% WER against Piper's 4.38%** and beat three of its four voices — but Piper's *irina* transcribes perfectly and we do not. Two caveats worth stating: the set is 79 words, so these gaps are one or two words; and ASR accuracy measures intelligibility, not naturalness. Frontend stress and orthoepy — homographs, ё restoration, the -ого/-его rule and its adverb exceptions, silent-consonant clusters: **15/16 correct**. **Timbre is darker than the source recordings**, and varies by voice. Measured in the 6-10 kHz band against Piper irina at -25.4 dB: dima -23.6 dB brighter than Piper masha -26.2 dB level with Piper sveta -33.8 dB noticeably darker, least training data (2.82 h) **Fricative artifact.** ж/ш/х carry narrowband spectral ridges where real speech has flat noise, +7.8 dB against +1.2 dB. Around 31 dB below programme level, so audible only at high volume. **Homographs are 93.8%, not perfect.** Known failure: "Это была настоящая мука" gives *flour* rather than *torment*. Pass explicit stress for critical text. **Prosody is deterministic.** Like all Kokoro models the style vector is chosen by phoneme-string length, so identical text always produces identical audio. base hexgrad/Kokoro-82M, kikiri-tts / StyleTTS2 recipe corpus 29.28 h, 16 speakers, 53% female, 47% full-band stage 1 5 epochs val mel 0.230 stage 2 10 epochs val mel 0.318 hardware 1x RTX A6000, ~$16 total Voices come from the Dialogs corpus (studio, consented actors, OpenRAIL). Russian LibriSpeech contributed phonetic coverage to the base model but supplies no shipped voice. Weights are OpenRAIL, code is Apache-2.0. hexgrad (Kokoro-82M), semidark (kikiri-tts), Den4ikAI (RUAccent), the OpenSLR Russian LibriSpeech contributors, and the Dialogs actors and authors. ONNX exports of the same checkpoints, same layout as [onnx-community/Kokoro-82M-v1.0-ONNX](https://huggingface.co/onnx-community/Kokoro-82M-v1.0-ONNX), so transformers.js / kokoro-js / onnxruntime can load this repo directly. onnx/model.onnx fp32 base (sveta, masha), 326 MB onnx/model_quantized.onnx q8 base (transformers.js dtype: "q8"), 138 MB onnx/model_fp16.onnx fp16 base (WebGPU only), 164 MB onnx/model_dima.onnx fp32 dima (male) onnx/model_dima_quantized.onnx q8 dima onnx/model_dima_fp16.onnx fp16 dima voices/{sveta,masha,dima}.bin style packs, raw float32 [510, 256] The model is language-blind (phoneme ids in, audio out), so the export is the same one used for the English Kokoro v1.0; the Russian G2P lives outside the model. Validation: ONNX vs PyTorch identical durations and 0.87–0.90 waveform correlation (ceiling ~0.84 between two PyTorch runs — the decoder injects noise); q8 vs fp32 identical durations and 0.58 dB mel-L1. import numpy as np, onnxruntime as ort from ru_g2p import RuG2P # this repo g2p = RuG2P() ipa, oov = g2p("Здравствуйте! Как ваши дела?") vocab = {c: i for c, i in __import__("json").load(open("config.json"))["vocab"].items()} ids = [vocab[c] for c in ipa if c in vocab] sess = ort.InferenceSession("onnx/model.onnx") # or model_quantized.onnx style = np.fromfile("voices/sveta.bin", dtype=np.float32).reshape(510, 256) audio = sess.run(None, dict( input_ids=np.array([[0, *ids, 0]], np.int64), style=style[len(ipa) - 1][None], speed=np.ones(1, np.float32), ))[0] Fully local — `onnxruntime-node` for inference and the `espeak-ng` npm package (WASM, ru voice) for G2P: import ort from "onnxruntime-node"; import ESpeakNg from "espeak-ng"; import fs from "node:fs"; const espeak = await ESpeakNg({ arguments: ["--phonout", "ipa.txt", "--ipa=3", "-q", "-v", "ru", text] }); const ipa = espeak.FS.readFile("ipa.txt", { encoding: "utf8" }); // map ipa chars to ids via config.json vocab, pad [0, ...ids, 0], // style = voices/<name>.bin row (len(ipa) - 1) * 256, then: const { waveform } = await session.run({ input_ids: new ort.Tensor("int64", BigInt64Array.from([0n, ...ids.map(BigInt), 0n]), [1, ids.length + 2]), style: new ort.Tensor("float32", styleRow, [1, 256]), speed: new ort.Tensor("float32", Float32Array.from([1]), [1]), }); Caveat: the Node G2P uses espeak's built-in Russian stress rules rather than RUAccent, so stress on some words (e.g. «дела́») is worse than the Python frontend. On a 55-word stress-heavy test set, Whisper base round-trip WER is ~27% (espeak G2P) vs ~22% (RUAccent G2P); on everyday sentences they are at parity. Porting RUAccent (which already runs on ONNX internally) to onnxruntime-node closes the gap.