← Practice · August 29, 2026
Setting up batch voice generation for Russian text with Yandex SpeechKit: multiple voices, per-character billing, and one script instead of manual work.
7 min readtools

A game needs character voices. A video needs narration. An audiobook needs a hundred chapters. A human voice actor will charge tens of thousands of rubles and take a week for this, while any text edit means making a new recording.
Speech synthesis handles the task differently: voice generation becomes part of the project build. Change a line, regenerate the file. Below is the working recipe I use to produce audio for my reference site.
Yandex SpeechKit currently offers some of the best Russian speech quality, and you can pay based on actual usage, per character. You need a service account API key with the synthesis role; everything else takes about thirty lines of code.
Get the key from the Yandex Cloud console: folder → “Service accounts” → create an account → assign the ai.speechkit-tts.user role → open the account card and select “Create new key” → API key (not an IAM token or a static access key). The value is shown only once.
Store it where it will not end up in the repository:
mkdir -p ~/.config/yandex
printf 'AQVN…your-key\n' > ~/.config/yandex/tts.key
chmod 600 ~/.config/yandex/tts.key
Test it immediately—this saves half an hour of confusion:
curl -s -o /tmp/проба.ogg -w 'HTTP %{http_code}\n' \
-H "Authorization: Api-Key $(cat ~/.config/yandex/tts.key)" \
--data-urlencode "text=connection test" -d "lang=ru-RU" -d "voice=jane" -d "format=oggopus" \
https://tts.api.cloud.yandex.net/speech/v1/tts:synthesize
200 means it works. 401 means the key does not have the synthesis role: a common mistake is using a speech recognition key, since they look very similar.
The idea is simple: use a table containing “who speaks—what they say—what the file is called” to generate MP3 files. Character lines are stored in an ordinary CSV file that can be edited in a spreadsheet.
файл,голос,текст
дед-01,zahar,Well then, granddaughter, let’s see who’s in charge here.
внучка-01,jane,I warned you, Grandpa.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Batch voice generation: реплики.csv → MP3 files in the звук/ directory."""
import csv, os, pathlib, subprocess, sys, time, urllib.parse, urllib.request
КЛЮЧ = open(os.path.expanduser('~/.config/yandex/tts.key')).read().strip()
URL = 'https://tts.api.cloud.yandex.net/speech/v1/tts:synthesize'
ПАПКА = pathlib.Path('звук'); ПАПКА.mkdir(exist_ok=True)
def синтез(текст, голос, скорость='1.0'):
данные = urllib.parse.urlencode({
'text': текст, 'lang': 'ru-RU', 'voice': голос,
'speed': скорость, 'format': 'oggopus'}).encode()
запрос = urllib.request.Request(URL, data=данные,
headers={'Authorization': f'Api-Key {КЛЮЧ}'})
for попытка in range(4): # Pauses handle 429 and 5xx errors
try:
return urllib.request.urlopen(запрос, timeout=120).read()
except urllib.error.HTTPError as e:
if e.code in (429, 500, 502, 503) and попытка < 3:
time.sleep(2 * (попытка + 1)); continue
raise
всего_знаков = 0
for строка in csv.DictReader(open('реплики.csv', encoding='utf-8')):
цель = ПАПКА / f"{строка['файл']}.mp3"
if цель.exists(): # Already generated—do not pay twice
continue
текст = строка['текст'].strip()
всего_знаков += len(текст)
ogg = ПАПКА / f"{строка['файл']}.ogg"
ogg.write_bytes(синтез(текст, строка['голос']))
subprocess.run(['ffmpeg', '-v', 'error', '-y', '-i', str(ogg),
'-b:a', '96k', str(цель)], check=True)
ogg.unlink()
print('done:', цель)
print(f'characters synthesized: {всего_знаков}')
python3 озвучка.py
SpeechKit has around fifteen voices. The female voices include jane, alena, and omazh; the male voices include filipp, zahar, and ermil. This is enough for game characters: two or three voices combined with speed changes produce noticeably different characters.
Speed is set with the speed parameter, from 0.1 to 3.0. Slowing a voice to 0.9 makes it sound more authoritative; speeding it up to 1.15 makes it sound more restless. It is a cheap trick that works: an old man and a teenager can use the same voice at different speeds.
Some voices support roles—neutral, good, and evil—through the emotion parameter. Not every voice supports them, so test the specific voice before building this into your pipeline.
Synthesis is billed by character count—you pay exactly for what you generate, with no subscription fee. This means you can estimate the budget in advance with a simple count:
python3 - <<'PY'
import csv
n = sum(len(с['текст']) for с in csv.DictReader(open('реплики.csv', encoding='utf-8')))
print('characters:', n, '≈ pages of text:', round(n / 1800, 1))
PY
Then multiply this by the current rate in Yandex’s price list. It changes, so I deliberately do not include a number here that will be false in six months. The general scale is this: a game script with a hundred lines contains several tens of thousands of characters, so the cost is comparable to a cup of coffee, not a voice actor’s fee.
Two habits save money. Do not regenerate audio that is already finished—in the script above, this is handled by a single line that checks whether the file exists. And do not process drafts: while the text is still being edited, listen to one or two key lines instead of the entire script.
The request length limit is about five thousand characters. A long chapter must be split by paragraphs, or by sentences if a paragraph is enormous. Join the pieces later with ffmpeg.
The response is not MP3. Version v1 returns oggopus or raw lpcm; ffmpeg creates the MP3 afterward. If you do not need MP3, keep the OGG files—they are smaller and browsers support them.
429 is not a failure; it is a rate issue. When generating hundreds of files in a batch, you will hit the rate limit. The retries with pauses in the script above solve the problem. You cannot leave them for “later”—you will discover the issue halfway through a large run.
Stress marks. Speech synthesis sometimes gets homographs wrong: “замок,” “стоит,” “дорога.” Fix this by placing a + before the stressed vowel: зам+ок. Listen through the finished files—typically, only two or three lines out of a hundred need corrections.
Numbers and abbreviations. “2026 г.” and “т. д.” are pronounced unpredictably. It is easier to write them out in words in the source text than to argue with the engine.
If you need a continuous track from the individual lines—for example, an audiobook chapter:
ls звук/глава-*.mp3 | sed "s/^/file '/;s/$/'/" > /tmp/список.txt
ffmpeg -v error -f concat -safe 0 -i /tmp/список.txt -c copy глава.mp3
ffprobe -v error -show_entries format=duration -of csv=p=0 глава.mp3
The last line prints the duration. This is useful when creating a table of contents with timestamps: the start time of each part equals the sum of the durations of all preceding parts.