РУКОВОДСТВО

Свой TTS для LiveKit Agents: дайте голосовому агенту любой голос

LiveKit Agents — популярный open source фреймворк для голосовых агентов в реальном времени: распознавание речи, языковая модель и озвучка текста работают в сессии, подключённой к комнате LiveKit. В нём есть плагины для многих провайдеров речи, а для любого другого нужен небольшой собственный класс.

В этом руководстве мы напишем такой класс для API озвучки SuperClonica. По нашим замерам он начинает стримить речь примерно через 300 мс и даёт агенту любой из 190 голосов каталога, включая персонажей и дикторов, или голос, который вы обучили сами.

Как устроен TTS-плагин

Плагин наследует tts.TTS и возвращает из synthesize объект ChunkedStream. Его метод _run отправляет запрос и передаёт байты звука в AudioEmitter, который сам декодирует Ogg, mp3 или wav и режет их на кадры. Мы проверили это на LiveKit Agents 1.8: если TTS не принимает текст потоком, сессия оборачивает его в StreamAdapter, который делит ответ модели на предложения. Каждое предложение — отдельный короткий запрос, и первое звучит, пока модель ещё пишет остальные.

pip install livekit-agents aiohttp

Плагин

Сохраните это как superclonica_tts.py рядом с агентом:

import os
import aiohttp
from livekit.agents import DEFAULT_API_CONNECT_OPTIONS, APIConnectOptions, APIStatusError, tts, utils

URL = "https://superclonica.com/api/v1/speech"


class SuperClonicaTTS(tts.TTS):
    def __init__(self, voice: str, language: str = "en"):
        super().__init__(capabilities=tts.TTSCapabilities(streaming=False), sample_rate=44100, num_channels=1)
        self.voice, self.language = voice, language
        self._session = None

    def synthesize(self, text: str, *, conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS):
        return SuperClonicaStream(tts=self, input_text=text, conn_options=conn_options)

    def session(self):
        if self._session is None:
            key = os.environ["SUPERCLONICA_KEY"]
            self._session = aiohttp.ClientSession(headers={"Authorization": "Bearer " + key})
        return self._session

    async def aclose(self):
        if self._session:
            await self._session.close()


class SuperClonicaStream(tts.ChunkedStream):
    async def _run(self, output_emitter: tts.AudioEmitter):
        engine = self._tts
        body = {"text": self.input_text, "voice": engine.voice, "language": engine.language, "stream": True}
        async with engine.session().post(URL, json=body) as answer:
            if answer.status != 200:
                raise APIStatusError(await answer.text(), status_code=answer.status)
            output_emitter.initialize(
                request_id=utils.shortuuid(), sample_rate=44100, num_channels=1, mime_type="audio/ogg"
            )
            async for chunk in answer.content.iter_chunked(4096):
                output_emitter.push(chunk)
            output_emitter.flush()

Запрос просит "stream": true, и API отвечает Ogg, который приходит прямо во время озвучки. Эмиттер декодирует его и пересэмплирует в 44,1 кГц, заявленные классом. Любой другой ответ вызывает APIStatusError: фреймворк повторяет 429 и ошибки сервера по своим настройкам соединения и сразу сдаётся на ошибках вроде неверного ключа.

Подключение к сессии

Передайте класс как tts при создании AgentSession вместе с плагинами распознавания речи и языковой модели, которые вы уже используете:

import os
from livekit.agents import AgentSession
from superclonica_tts import SuperClonicaTTS

session = AgentSession(
    stt=...,  # your speech recognition plugin
    llm=...,  # your language model plugin
    tts=SuperClonicaTTS(voice=os.environ["VOICE_ID"], language="en"),
)

Задайте SUPERCLONICA_KEY — ключ со страницы API озвучки — и VOICE_ID — ID со страницы выбранного голоса. Всё остальное в агенте остаётся как было.

Что нужно знать об ограничениях

API работает по обычному HTTPS с потоковой передачей звука, а не через WebSocket, и не возвращает тайминги слов, поэтому выровненные субтитры выключены. Он говорит на 31 языке, но пока не по-китайски. Тариф даёт секунды речи в каждом 10-минутном окне на все звонки вместе: бесплатный ключ — минуту в день для разработки, «Старт» за $5 в месяц — до 3 минут на окно, Pro — 20 минут, этого хватает на несколько оживлённых звонков одновременно.