Синхронизация речи и действий: голосовой AI ассистент

История о том, как я пытаюсь создать голосового AI помощника для моего 5-летнего сына.

vfwemmvf0kwhy6bgsjp-7fjjsx0.jpeg

Создание AI помощника — идея не новая, особенно с учетом массового распространения ИИ в последний год и появления голосового ассистента от OpenAI и их Realtime API - которое позволяет разработчикам создавать мультимодальные интерфейсы с низкой задержкой преобразования речи в речь.

Хотя API OpenAI предлагает потрясающие возможности, высокая стоимость ($100 за 1 млн входных токенов и $200 за 1 млн выходных токенов) подталкивает к поиску более доступных решений. Поэтому я обратил внимание на опенсорсный проект LiveKit, предлагающий масштабируемую связь в реальном времени. Одной из интересных функций — возможность интеграции ИИ агентов. И я решил попробовать использовать их решение, для создания госового помощника.

Типичный pipeline голосового агента в LiveKit выглядит следующим образом:

Источник: https://docs.livekit.io/agents/quickstarts/voice-agent/

Если просто: голосовой поток пользователя преобразуется в текст. Полученный текст передается в большую языковую модель (LLM), которая, следуя заданным инструкциям (промпту), генерирует текстовый ответ. Текстовый ответ, сгенерированный LLM, преобразуется в аудиопоток. Сгенерированный аудиопоток воспроизводится.

В моем проекте используются следующие компоненты LiveKit:

  1. VAD:   детектор голосовой активности — Silero VAD .

  2. SST (Speech-to-Text):  Мультиязычная модель nova-2-general от Deepgram.

  3. LLM (Large Language Model): GPT-4o от OpenAI. Однако, LiveKit поддерживает и другие модели, совместимые с OpenAI API (Groq, Perplexity, TogetherAI и др.).

  4. TTS (Text-to-Speech):  Использован сервис OpenAI с голосом «alloy». Возможны и другие варианты (Deepgram, и др.).

Цели проекта

Моя цель — создать интерактивного учителя, способного не только объяснять материал, но и управлять интерактивными инструментами в режиме реального времени, подобно живому учителю, который одновременно рассказывает и показывает. Ключевая задача — реализовать голосового помощника с «потоковым» управлением интерактивными инструментами .

Математика с интерактивной таблицей

Чтобы проверить идею управления интерактивными инструментами с помощью голосового помощника, я выбрал упражнение из учебника Oxford International Primary Maths на тему «десятки». Помощник должен объяснять материал и одновременно используя интерактивную таблицу выделять столбцы, строки или отдельные ячейки, иллюстрируя свои объяснения.

2ismrkk_j6yqp23eijzwhphq_qe.png

Этапы реализации:

1. Создание интерактивной таблицы

На Vue.js создана интерактивная таблица 10×10, позволяющая выделять столбцы, строки и отдельные ячейки.

g6jh2tz8vvxh1gnyxvn6coa0l84.png

2. Постановка задачи для LLM

LLM получила инструкцию: объяснить пятилетнему ребенку понятие «десятки» и как с ними считать, используя интерактивную таблицу.

Описание API для взаимодействия с таблицей:

Я описал взаимодействие с интерактивной таблицей через OpenAPI. Например, для выделения столбца должен быть использован вызов: http POST /highlight-column?column=1.

###API You may call interactive table use openAPI:
    openapi: 3.0.0
    info:
      title: Interactive Table API
      version: 1.0.0
      description: API for managing a 10x10 interactive number table
    paths:
      /highlight-column:
        post:
          summary: Highlight Column
          description: Highlights the specified column in the table
          operationId: highlightColumn
          parameters:
            - name: column
              in: query
              description: Column number to highlight (1-10)
              required: true
              schema:
                type: integer
                minimum: 1
                maximum: 10
          responses:
            '200':
              description: Successfully highlighted the column
      /highlight-row:
        post:
          summary: Highlight Row
          description: Highlights the specified row in the table
          operationId: highlightRow
          parameters:
            - name: row
              in: query
              description: Row number to highlight (1-10)
              required: true
              schema:
                type: integer
                minimum: 1
                maximum: 10
          responses:
            '200':
              description: Successfully highlighted the row
      /highlight-number:
        post:
          summary: Highlight Number
          description: Highlights the specified number in the table
          operationId: highlightNumber
          parameters:
            - name: number
              in: query
              description: Number to highlight (1-100)
              required: true
              schema:
                type: integer
                minimum: 1
                maximum: 100
          responses:
            '200':
              description: Successfully highlighted the number
              
###CALL API          
If you need to set an example for a child, always use an interactive table, for example:
```http POST /highlight-column?column=1```

Ответ LLM:  LLM генерирует вызов функции непосредственно в ответе, а не отдельным процессом.

Пример ответа

Пример ответа

3. Синхронизация вызова функции и воспроизведения аудио

Основная сложность заключалась в синхронизации вызова функции с воспроизведением аудио. Синхронизация должна происходить именно во время воспроизведения, а не во время генерации ответа LLM или преобразования текста в речь.

Архитектура потока данных

Базовый поток данных в системе выглядит следующим образом:

STT → LLM → TTS → PlayoutAudio

Для реализации синхронизации потребовалось внести изменения в исходный код проекта LiveKit на следующих этапах LLM — TTS — PlayoutAudio. А именно, найти и пробросить на все дальнейшие этапы «вызов функции».

  1. Анализ ответа от LLM: На этапе получения ответа от LLM (в режиме stream) добавлена посимвольная проверка на наличие маски вызова функции, которая определяет, есть ли в тексте вызов функции. Найденная функция собирается как отдельное предложение.

  2. Обработка TTS: Текст, генерируемый LLM (в режиме stream), накапливается в буфере и периодически, когда набирается достаточно данных (достигнута минимальная длина предложения), отправляет на генерацию аудио.

    При этом, если при разбивке на предложения мы встречаем «вызов функции», то следующему предложению мы добавляем отдельный параметр call_tools, а сам «вызов функции» не оптравялется в TTS (озвучивать же его не надо).

    Таким образом это эмулируют стриминг, разбивая текст на предложения и отправляя каждое предложение в TTS. Это позволяет обрабатывать длинные тексты, обеспечивая постепенное получение аудиофреймов.

  3. Воспроизведение и вызов функции: На этапе воспроизведения (PlayoutAudio) происходит проверка call_tools в каждом аудиофрейме. Если call_tools содержит данные, то осуществляется вызов соответствующей функции в момент начала воспроизведения текущего аудиофрейма. Это обеспечивает синхронизацию с содержанием, произносимым в данный момент.

Схематичное представление (сгенериовано с помощью Claude 3.5 Sonnet - спасибо ИИ за это)

Схематичное представление (сгенериовано с помощью Claude 3.5 Sonnet — спасибо ИИ за это)

Таким образом, данная архитектура позволяет достигнуть синхронизации: разбиение текста на предложения и обработка каждого предложения в отдельном экземпляре — ключевой элемент реализации, позволяющий связать команды с аудио и обеспечить их синхронное выполнение.

P.S. Разбиение на отдельные предложения уже был реализован в LiveKit для работы с нестриминговыми TTS.

Почему стандартный вызов функций не подошёл

Как вы знаете LLM может вызывать функции, чтобы управлять внешними сервисами и на самом деле LiveKit реализовало поддержку call function.

Однако, стандартный вызов функций LLM — не обеспечивал необходимую синхронизацию, проблемы возникали при множественных вызовах, не позволяя четко определить момент их исполнения.

Небольшой тест работы вызовов функций в «потоковом» режиме

Cмотреть тут — telegram

Это лишь первые эксперименты, и я планирую дальше развивать данный проект. Любые ваши предложения и критика помогут мне улучшить ассистента быстрее. Спасибо!

© Habrahabr.ru