Руководство по синтезу речи
Подготовка
В этом руководстве можно посмотреть, как и для каких целей используются методы синтеза речи с примерами.
Эндпоинт для синтеза — api.tinkoff.ai:443
.
Для авторизации в сервисах получите ключи и вставьте их в переменные среды.
export VOICEKIT_API_KEY="PUT_YOUR_API_KEY_HERE"
export VOICEKIT_SECRET_KEY="PUT_YOUR_SECRET_KEY_HERE"Склонируйте репозиторий с примерами.
git clone --recursive https://github.com/Tinkoff/voicekit-examples.git
Установите зависимости.
sudo apt-get install python3 python-pyaudio python3-pyaudio
sudo python3 -m pip install -r python/requirements/all.txt
cd python/snippets
В сервисе есть 3 метода для синтеза речи:
- Потоковый синтез речи (StreamingSynthesize): для потокового синтеза.
- Непотоковый синтез речи (Synthesize): для синтеза аудио целиком.
- Список доступных голосов (ListVoices): для получения списка голосов.
Потоковый синтез речи
Метод нужен для синтеза речи в реальном времени — с помощью него получать синтезированный аудиопоток для произвольного текста по мере синтеза.
Пример 1. Несжатое аудио
./tts_streaming_synthesize_linear16_to_wav.py
Синтезируем в Linear16 48KHz и сохраняем в
WAV
.Импортируем модули.
from tinkoff.cloud.tts.v1 import tts_pb2_grpc, tts_pb2
from auth import authorization_metadata
import grpc
import os
import waveПолучаем конфигурацию.
# можно получать из переменных среды или заменить в сниппете
endpoint = os.environ.get("VOICEKIT_ENDPOINT") or "api.tinkoff.ai:443"
api_key = os.environ["VOICEKIT_API_KEY"]
secret_key = os.environ["VOICEKIT_SECRET_KEY"]
sample_rate = 48000Создаем запрос.
def build_request():
return tts_pb2.SynthesizeSpeechRequest(
input=tts_pb2.SynthesisInput(
text="И мысли тоже тяжелые и медлительные, падают неторопливо и редко "
"одна за другой, точно песчинки в разленившихся песочных часах."
),
audio_config=tts_pb2.AudioConfig(
audio_encoding=tts_pb2.LINEAR16,
sample_rate_hertz=sample_rate,
),
)Открываем
.wav
-файл для записи синтезированного аудио.Шлём запрос и записываем полученные семплы.
Попутно печатаем приблизительную длительность аудио:
with wave.open("synthesized.wav", "wb") as f:
f.setframerate(sample_rate)
f.setnchannels(1)
f.setsampwidth(2)
stub = tts_pb2_grpc.TextToSpeechStub(
grpc.secure_channel(endpoint, grpc.ssl_channel_credentials()))
request = build_request()
metadata = authorization_metadata(api_key, secret_key, "tinkoff.cloud.tts")
responses = stub.StreamingSynthesize(request, metadata=metadata)
for key, value in responses.initial_metadata():
if key == "x-audio-duration-seconds":
print("Estimated audio duration is {:.2f} seconds".format(float(value)))
break
for stream_response in responses:
f.writeframes(stream_response.audio_chunk)При синтезировании больших объемов текста нужно подобрать размеры сообщения, отвечающие размеру аудио:
# increase this number if you are trying to synthesize really long text
ONE_HUNDRED_MEGABYTE_BUFFER = 100 * 1024 * 1024
grpc.secure_channel(
endpoint,
grpc.ssl_channel_credentials(),
options=[
("grpc.max_message_length", ONE_HUNDRED_MEGABYTE_BUFFER),
("grpc.max_receive_message_length", ONE_HUNDRED_MEGABYTE_BUFFER),
],
)
Пример 2. Потоковое воспроизведение
./tts_streaming_synthesize_linear16_playback.py
Для удобства тестирования синтезируем и воспроизводим на звуковой карте входящий поток.
Импортируем
pyaudio
:import pyaudio
Открываем устройство воспроизведения на звуковой карте.
pyaudio_lib = pyaudio.PyAudio()
f = pyaudio_lib.open(output=True, channels=1, format=pyaudio.paInt16, rate=sample_rate)Пишем семплы в устройство.
for stream_response in responses:
f.write(stream_response.audio_chunk)
Пример 3. Синтез в A-law (PCMA)
./tts_streaming_synthesize_a_law_playback.py
A-law позволяет сократить сетевые издержки за счет того, что один семпл занимает 1 байт — с компандированием.
В Python есть встроенный модуль для работы с различными PCM кодировками — audioop
.
Импортируем модуль.
import audioop
Для синтеза в A-law указываем кодировку в поле запроса.
audio_encoding=tts_pb2.ALAW,
Для конвертации семплов из A-law в Linear16 достаточно использовать функцию
audioop.alaw2lin
с аргументомwidth=2
— 2 байта на семпл.pcm_chunk = audioop.alaw2lin(stream_response.audio_chunk, 2)
Пример 4. Синтез в Opus
./tts_streaming_synthesize_raw_opus_playback.py
Синтезируем в Opus и воспроизводим на звуковой карте.
Sample rate для Opus не имеет значения, но примерная длительность аудио передается в семплах исходя из
заданного значения sample rate
.
Импортируем модуль для декодирования Opus.
import opuslib
Актуализируем
sample rate
.sample_rate = 16000
Выставляем кодек.
audio_encoding=tts_pb2.RAW_OPUS,
Инициализируем декодер.
opus_decoder = opuslib.Decoder(sample_rate, 1)
Пишем декодированные фреймы в устройство.
f.write(opus_decoder.decode(stream_response.audio_chunk, 5760))
5760
— максимальный размер фрейма: в адекватном биндинге libopus
для Python вычислялся бы автоматически
в зависимости от частоты дискретизации.
Пример 5. Использование SSML
./tts_streaming_synthesize_ssml_linear16_playback.py
Подробнее об использовании SSML-тегов
Для синтеза SSML в SynthesisInput
используется поле ssml
вместо text
, а сам текст оборачивается в тег <speak>
:
def build_request():
return tts_pb2.SynthesizeSpeechRequest(
input=tts_pb2.SynthesisInput(
ssml="""
<speak>
<voice name="anna">
<p>
<s>
<emphasis level="strong">Оригинальная</emphasis> мысль?
</s>
<s>
Нет ничего легче.
</s>
</p>
<break time='300ms'/>
<p>
<s>
<voice name="anna">
Библиотеки просто набиты ими.
</voice>
</s>
</p>
</voice>
</speak>
"""
),
audio_config=tts_pb2.AudioConfig(
audio_encoding=tts_pb2.LINEAR16,
sample_rate_hertz=sample_rate,
),
)
Пример 6. Выбор голоса
./tts_streaming_synthesize_voice_selection_linear16_playback.py
Чтобы выбрать голос,
в SynthesizeSpeechRequest
явно задается поле voice
:
def build_request():
return tts_pb2.SynthesizeSpeechRequest(
input=tts_pb2.SynthesisInput(
text="Привет! Я Алёна. Я помогу в озвучке книг, новостей, образователь"
"ных курсов, а также могу быть твоим напарником для медитации."
),
audio_config=tts_pb2.AudioConfig(
audio_encoding=tts_pb2.LINEAR16,
sample_rate_hertz=sample_rate,
),
voice=tts_pb2.VoiceSelectionParams(
name="anna"
),
)
Пример 7. Получение метаданных
./tts_streaming_synthesize_initial_metadata.py
Синтез отправляет три заголовка в начальных метаданных:
x-request-id
— уникальный идентификатор запроса.x-audio-num-samples
— примерное число семплов аудио.x-audio-duration-seconds
— примерная длительность аудио в секундах.
responses = stub.StreamingSynthesize(request, metadata=metadata)
for key, value in responses.initial_metadata():
if key == "x-request-id":
print("RequestId is {}".format(value))
if key == "x-audio-num-samples":
print("Estimated audio duration is {} samples".format(value))
if key == "x-audio-duration-seconds":
print("Estimated audio duration is {:.2f} seconds".format(float(value)))
При обращении в службу поддержки желательно указывать значение x-request-id
запроса.
Непотоковый синтез речи
Пример 8. Несжатое аудио
./tts_synthesize_linear16_to_wav.py
Для синтеза аудио целиком используется метод Synthesize. Достаточно передать запрос и метаданные в том же формате, что и для метода StreamingSynthesize:
stub = tts_pb2_grpc.TextToSpeechStub(grpc.secure_channel(endpoint, grpc.ssl_channel_credentials()))
request = build_request()
metadata = authorization_metadata(api_key, secret_key, "tinkoff.cloud.tts")
response = stub.Synthesize(request, metadata=metadata)
Аудио находится в поле audio_content
ответа:
with wave.open("synthesized.wav", "wb") as f:
f.setframerate(sample_rate)
f.setnchannels(1)
f.setsampwidth(2)
f.writeframes(response.audio_content)
Пример 9. Воспроизведение несжатого аудио
./tts_synthesize_linear16_playback.py
Для воспроизведения аудио на звуковой карте достаточно передать содержимое поля audio_content
в поток
воспроизведения:
pyaudio_lib = pyaudio.PyAudio()
f = pyaudio_lib.open(output=True, channels=1, format=pyaudio.paInt16, rate=sample_rate)
f.write(response.audio_content)
f.stop_stream()
f.close()
Пример 10. REST API
./tts_synthesize_rest_linear16_playback.py
Импортируем модуль:
requests
— для отправки HTTP-запросов.import requests
httpx
— для отправки HTTP2-асинхронных запросов.import httpx
Создаем запрос одним из способов:
Используя
google.protobuf.json_format.MessageToDict
.from tinkoff.cloud.tts.v1 import tts_pb2
from google.protobuf.json_format import MessageToDict
def build_request_from_pb():
pb_request = tts_pb2.SynthesizeSpeechRequest(
input=tts_pb2.SynthesisInput(
text="И мысли тоже тяжелые и медлительные, падают неторопливо и редко одна за другой, точно песчинки "
"в разленившихся песочных часах.",
),
audio_config=tts_pb2.AudioConfig(
audio_encoding=tts_pb2.LINEAR16,
sample_rate_hertz=sample_rate,
),
voice=tts_pb2.VoiceSelectionParams(
name="anna",
),
)
return MessageToDict(pb_request)Сразу — в формате словаря:
def build_request():
return {
"input": {
"text": "И мысли тоже тяжелые и медлительные, падают неторопливо и редко одна за другой, точно песчинки "
"в разленившихся песочных часах.",
},
"audioConfig": {
"audioEncoding": "LINEAR16",
"sampleRateHertz": sample_rate,
},
"voice": {
"name": "anna",
}
}
Для синтеза аудио целиком через REST API достаточно отправить POST-запрос к методу
/v1/tts:synthesize
с данными в форматеJSON
.Подключите HTTP2 и в метаданных передайте поля авторизации, как в методе StreamingSynthesize.
Сгенерировать аудио размером больше 1 Мб в REST API можно только при использовании протокола HTTP/2 — например, через HTTPX. При использовании HTTP/1 запросы размером большее 1 Мб не исполнятся.
В gRPC API таких ограничений нет.
Пример для модуля
requests
:request = build_request()
metadata = authorization_metadata(api_key, secret_key, "tinkoff.cloud.tts", type=dict)
response = requests.post(f"http{'s' if endpoint.endswith('443') else ''}://{endpoint}/v1/tts:synthesize", json=request, headers=metadata)Пример для модуля
httpx
:async def main():
async with httpx.AsyncClient(http2=True) as client:
request = build_request()
metadata = authorization_metadata(api_key, secret_key, "tinkoff.cloud.tts", type=dict)
response = await client.post(f"http{'s' if endpoint.endswith('443') else ''}://{endpoint}/v1/tts:synthesize", json=request, headers=metadata)
Синтезированное аудио закодировано в Base64-строке в поле audio_content
ответа:
if response.status_code != 200:
print(f"REST failed with HTTP code {response.status_code}\nHeaders: {response.headers}\nBody: {response.text}")
else:
response = response.json()
pyaudio_lib = pyaudio.PyAudio()
f = pyaudio_lib.open(output=True, channels=1, format=pyaudio.paInt16, rate=sample_rate)
f.write(base64.b64decode(response["audio_content"]))
f.stop_stream()
f.close()
Пример 11. REST API через cURL
./tts_synthesize_rest_linear16_playback.sh
Генерируем авторизационный JWT с помощью shell-скрипта gen_jwt.sh:
TEN_MINUTES=600
JWT=$(
./gen_jwt.sh --api_key "${VOICEKIT_API_KEY}" \
--secret_key "${VOICEKIT_SECRET_KEY}" \
--scope tinkoff.cloud.tts \
--exp $(("$(date +%s)" + "${TEN_MINUTES}"))
)Фиксируем
sample rate
.SAMPLE_RATE=24000
Создаем JSON-запрос одним из способов:
Используя jq:
request="$(
jq --null-input \
--arg input_text 'Мы подсчитали, что шанс «один на миллион» выпадает в девяти случаях из десяти.' \
--arg audio_encoding 'LINEAR16' \
--arg sample_rate "${SAMPLE_RATE}" \
--arg voice_name 'anna' \
'
.input.text = $input_text |
.audioConfig.audioEncoding = $audio_encoding |
.audioConfig.sampleRateHertz = ($sample_rate | tonumber) |
.voice.name = $voice_name
'
)"Cразу:
request='{
"input": {
"text": "Мы подсчитали, что шанс «один на миллион» выпадает в девяти случаях из десяти."
},
"audioConfig": {
"audioEncoding": "LINEAR16",
"sampleRateHertz": 48000
},
"voice": {
"name": "anna"
}
}'
С помощью cURL отправляем POST-запрос к методу
/v1/tts:synthesize
, не забывая про токен авторизации в метаданных.Синтезированное аудио закодировано в Base64-строке в поле
audio_content
ответа. Достать его можно при помощи jq.
После этого аудио можно декодировать из Base64 и воспроизвести при помощи команды play
из SoX:
curl --header "Content-Type: application/json" \
--header "Authorization: Bearer ${JWT}" \
--request POST \
--data "${request}" \
https://api.tinkoff.ai:443/v1/tts:synthesize | jq --raw-output .audio_content | base64 -d | play --type s16 --rate "${SAMPLE_RATE}" --channels 1 -
Список доступных голосов
Пример 12. gRPC API
./tts_list_voices.py
Чтобы получить список доступных голосов, достаточно вызвать метод ListVoices
. В метаданных нужно
передать данные для авторизации, как в примерах выше:
stub = tts_pb2_grpc.TextToSpeechStub(grpc.secure_channel(endpoint, grpc.ssl_channel_credentials()))
request = tts_pb2.ListVoicesRequest()
metadata = authorization_metadata(api_key, secret_key, "tinkoff.cloud.tts")
response = stub.ListVoices(request, metadata=metadata)
Список доступных голосов находится в поле voices
ответа:
print("Allowed voices:")
for voice in sorted(response.voices, key=lambda voice: voice.name):
print(f"- {voice.name}")
Пример 13. REST API
./tts_list_voices_rest.py
Импортируем модуль:
requests
— для отправки HTTP-запросов.import requests
httpx
— для отправки HTTP2-асинхронных запросов.import httpx
Для получения списка доступных голосов достаточно отправить GET-запрос к методу
/v1/tts:list_voices
:Пример для модуля
requests
:metadata = authorization_metadata(api_key, secret_key, "tinkoff.cloud.tts", type=dict)
response = requests.get(f"http{'s' if endpoint.endswith('443') else ''}://{endpoint}/v1/tts:list_voices", headers=metadata)Пример для модуля
httpx
:async with httpx.AsyncClient(http2=True) as client:
metadata = authorization_metadata(api_key, secret_key, "tinkoff.cloud.tts", type=dict)
response = await client.get(f"http{'s' if endpoint.endswith('443') else ''}://{endpoint}/v1/tts:list_voices", headers=metadata)
В ответе передается JSON со списком голосов:
if response.status_code != 200:
print(f"REST failed with HTTP code {response.status_code}\nHeaders: {response.headers}\nBody: {response.text}")
else:
response = response.json()
print("Allowed voices:")
for voice in sorted(response["voices"], key=lambda voice: voice["name"]):
print(f"- {voice['name']}")
Пример 14. REST API через cURL
./tts_list_voices_rest.sh
Генерируем авторизационный JWT с помощью shell-скрипта gen_jwt.sh.
```shell
TEN_MINUTES=600
JWT=$(
./gen_jwt.sh --api_key "${VOICEKIT_API_KEY}" \
--secret_key "${VOICEKIT_SECRET_KEY}" \
--scope tinkoff.cloud.tts \
--exp $(("$(date +%s)" + "${TEN_MINUTES}"))
)
```С помощью cURL отправляем GET-запрос к методу
/v1/tts:list_voices
, не забывая про токен авторизации в метаданных.
Ответ можно парсить при помощи jq
:
curl --header "Content-Type: application/json" \
--header "Authorization: Bearer ${JWT}" \
--request GET \
https://api.tinkoff.ai:443/v1/tts:list_voices | jq