paint-brush
Как создать свою собственную исповедь искусственного интеллекта: как добавить голос к LLMк@slavasobolev
1,140 чтения
1,140 чтения

Как создать свою собственную исповедь искусственного интеллекта: как добавить голос к LLM

к Iaroslav Sobolev11m2024/07/14
Read on Terminal Reader

Слишком долго; Читать

В конце февраля на Бали прошел фестиваль [Лампу](https://lampu.org/), устроенный по принципам знаменитого Burning Man. Нас вдохновила идея католических конфессионалов и возможности нынешнего LLM. Мы построили собственную исповедальню искусственного интеллекта, где каждый мог поговорить с искусственным интеллектом.
featured image - Как создать свою собственную исповедь искусственного интеллекта: как добавить голос к LLM
Iaroslav Sobolev HackerNoon profile picture
0-item
1-item

Хотя OpenAI откладывает выпуск расширенных голосовых режимов для ChatGPT, я хочу рассказать, как мы создали голосовое приложение LLM и интегрировали его в интерактивный стенд.

Поговорите с ИИ в джунглях

В конце февраля на Бали прошел фестиваль Лампу , устроенный по принципам знаменитого Burning Man. По традиции участники создают собственные инсталляции и арт-объекты.


Я и мои друзья из лагеря 19:19 , вдохновленные идеей католических исповеданий и возможностями нынешних магистратур, пришли к идее построить собственную ИИ-исповедальню, где каждый мог бы поговорить с искусственным интеллектом.


Вот как мы это себе представляли в самом начале:

  • Когда пользователь входит в кабинку, мы определяем, что нам нужно начать новый сеанс.


  • Пользователь задает вопрос, а ИИ слушает и отвечает. Мы хотели создать доверительную и приватную среду, где каждый мог бы открыто обсуждать свои мысли и опыт.


  • Когда пользователь покидает комнату, система завершает сеанс и забывает все детали разговора. Это необходимо для сохранения конфиденциальности всех диалогов.

Доказательство концепции

Чтобы протестировать концепцию и начать экспериментировать с подсказкой для LLM, я за один вечер создал наивную реализацию:

  • Слушайте микрофон.
  • Распознавайте речь пользователя с помощью модели преобразования речи в текст (STT) .
  • Сгенерируйте ответ через LLM .
  • Синтезируйте голосовой ответ, используя модель преобразования текста в речь (TTS) .
  • Воспроизведите ответ пользователю.



Для реализации этой демонстрации я полностью полагался на облачные модели от OpenAI: Whisper , GPT-4 и TTS . Благодаря отличной библиотеке voice_recognition я построил демо всего за несколько десятков строк кода.


 import os import asyncio from dotenv import load_dotenv from io import BytesIO from openai import AsyncOpenAI from soundfile import SoundFile import sounddevice as sd import speech_recognition as sr load_dotenv() aiclient = AsyncOpenAI( api_key=os.environ.get("OPENAI_API_KEY") ) SYSTEM_PROMPT = """ You are helpfull assistant. """ async def listen_mic(recognizer: sr.Recognizer, microphone: sr.Microphone): audio_data = recognizer.listen(microphone) wav_data = BytesIO(audio_data.get_wav_data()) wav_data.name = "SpeechRecognition_audio.wav" return wav_data async def say(text: str): res = await aiclient.audio.speech.create( model="tts-1", voice="alloy", response_format="opus", input=text ) buffer = BytesIO() for chunk in res.iter_bytes(chunk_size=4096): buffer.write(chunk) buffer.seek(0) with SoundFile(buffer, 'r') as sound_file: data = sound_file.read(dtype='int16') sd.play(data, sound_file.samplerate) sd.wait() async def respond(text: str, history): history.append({"role": "user", "content": text}) completion = await aiclient.chat.completions.create( model="gpt-4", temperature=0.5, messages=history, ) response = completion.choices[0].message.content await say(response) history.append({"role": "assistant", "content": response}) async def main() -> None: m = sr.Microphone() r = sr.Recognizer() messages = [{"role": "system", "content": SYSTEM_PROMPT}] with m as source: r.adjust_for_ambient_noise(source) while True: wav_data = await listen_mic(r, source) transcript = await aiclient.audio.transcriptions.create( model="whisper-1", temperature=0.5, file=wav_data, response_format="verbose_json", ) if transcript.text == '' or transcript.text is None: continue await respond(transcript.text, messages) if __name__ == '__main__': asyncio.run(main())


Проблемы, которые нам предстояло решить, сразу стали очевидны после первых тестов этой демо-версии:

  • Задержка ответа . В наивной реализации задержка между вопросом пользователя и ответом составляет 7–8 секунд и более. Это нехорошо, но очевидно, что существует множество способов оптимизировать время отклика.


  • Окружающий шум . Мы обнаружили, что в шумной обстановке мы не можем полагаться на то, что микрофон автоматически определит, когда пользователь начал и закончил говорить. Распознавание начала и конца фразы ( конечная точка ) — нетривиальная задача. Добавьте к этому шумную атмосферу музыкального фестиваля, и становится ясно, что необходим концептуально другой подход.


  • Имитируйте живой разговор . Мы хотели дать пользователю возможность прерывать работу ИИ. Чтобы добиться этого, нам придется оставить микрофон включенным. Но в этом случае нам пришлось бы отделять голос пользователя не только от фоновых звуков, но и от голоса ИИ.


  • Обратная связь . Из-за задержки ответа нам иногда казалось, что система зависла. Мы поняли, что нам нужно сообщить пользователю, как долго будет обрабатываться ответ.


У нас был выбор, как решить эти проблемы: искать подходящее инженерное или продуктовое решение.

Продумывание UX стенда

Прежде чем мы приступили к написанию кода, нам нужно было решить, как пользователь будет взаимодействовать со стендом:

  • Нам следует решить, как обнаружить нового пользователя в кабинке, чтобы сбросить историю прошлых диалогов.


  • Как распознать начало и конец речи пользователя и что делать, если он хочет прервать ИИ.


  • Как реализовать обратную связь при задержке ответа ИИ.


Для обнаружения нового пользователя в кабинке мы рассмотрели несколько вариантов: датчики открытия дверей, датчики веса пола, датчики расстояния и модель камера + YOLO. Датчик расстояния за спиной нам показался самым надежным, так как исключал случайные срабатывания, например, когда дверь закрыта недостаточно плотно, и не требовал сложной установки, в отличие от датчика веса.


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


У нас было много разных идей по реализации обратной связи при обработке запроса. Мы остановились на варианте с экраном, на котором видно, что делает система: слушает микрофон, обрабатывает вопрос или отвечает.


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


Во время инсталляции и на фестивале


В итоге окончательный пользовательский поток получился таким:

  • Пользователь заходит в кабинку. За его спиной срабатывает датчик расстояния, и мы приветствуем его.


  • Пользователь нажимает красную кнопку, чтобы начать диалог. Слушаем микрофон, пока нажата кнопка. Когда пользователь отпускает кнопку, мы начинаем обработку запроса и указываем его на экране.


  • Если пользователь хочет задать новый вопрос, пока ИИ отвечает, он может нажать кнопку еще раз, и ИИ немедленно перестанет отвечать.


  • Когда пользователь покидает кабинку, датчик расстояния срабатывает снова, и мы очищаем историю диалогов.

Архитектура


Ардуино следит за состоянием датчика расстояния и красной кнопки. Все изменения он отправляет на наш бэкенд через HTTP API, что позволяет системе определить, вошел ли пользователь в кабинку или вышел из нее и нужно ли активировать прослушивание микрофона или начать генерировать ответ.


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


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

Аппаратное обеспечение

Как закодировать скетч для Arduino, правильно соединить датчик расстояния и кнопку и собрать все это в стенде – тема для отдельной статьи. Давайте кратко рассмотрим, что у нас получилось, не вдаваясь в технические подробности.


Мы использовали Arduino, точнее модель ESP32 со встроенным модулем Wi-Fi. Микроконтроллер был подключен к той же сети Wi-Fi, что и ноутбук, на котором выполнялась серверная часть.



Полный список оборудования, которое мы использовали:

Бэкэнд

Основными компонентами конвейера являются преобразование речи в текст (STT), LLM и преобразование текста в речь (TTS). Для каждой задачи доступно множество различных моделей как локально, так и через облако.



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


Теперь давайте более подробно рассмотрим каждый компонент конвейера.

Распознавание речи

Многие современные устройства уже давно поддерживают распознавание речи. Например, API Apple Speech доступен для iOS и macOS, а API Web Speech — для браузеров.


К сожалению, по качеству они сильно уступают Whisper или Deepgram и не могут автоматически определить язык.


Чтобы сократить время обработки, лучшим вариантом является распознавание речи в реальном времени по мере того, как говорит пользователь. Вот несколько проектов с примерами их реализации: шепот_стриминг , шепот.cpp


На нашем ноутбуке скорость распознавания речи при таком подходе оказалась далека от реальной. После нескольких экспериментов мы остановились на облачной модели Whisper от OpenAI.

LLM и оперативное проектирование

Результатом модели «Речь в текст» из предыдущего шага является текст, который мы отправляем в LLM с историей диалогов.


При выборе LLM мы сравнивали GPT-3.5. GPT-4 и Клод. Оказалось, что ключевым фактором была не столько конкретная модель, сколько ее комплектация. В конечном итоге мы остановились на GPT-4, ответы которого нам понравились больше остальных.


Кастомизация подсказки для моделей LLM стала отдельным видом искусства. В Интернете есть множество руководств, как настроить свою модель так, как вам нужно:



Нам пришлось много экспериментировать с настройками подсказки и температуры , чтобы модель реагировала увлекательно, лаконично и с юмором.

Текст в речь

Мы озвучиваем полученный от LLM ответ, используя модель Text-To-Speech, и воспроизводим его пользователю. Этот шаг был основной причиной задержек в нашей демо-версии.


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


Озвучивание отдельных предложений


  • Сделайте запрос в LLM.


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


  • Отправьте сгенерированное предложение в модель TTS и воспроизведите результат пользователю. На этом этапе необходимо убедиться в отсутствии состояния гонки в порядке воспроизведения.


  • Повторяйте предыдущий шаг до конца ответа LLM.


Время, пока пользователь прослушивает исходный фрагмент, мы используем, чтобы скрыть задержку обработки остальных частей ответа от LLM. Благодаря такому подходу задержка ответа возникает только в начале и составляет ~3 секунды.


 async generateResponse(history) { const completion = await this.ai.completion(history); const chunks = new DialogChunks(); for await (const chunk of completion) { const delta = chunk.choices[0]?.delta?.content; if (delta) { chunks.push(delta); if (chunks.hasCompleteSentence()) { const sentence = chunks.popSentence(); this.voice.ttsAndPlay(sentence); } } } const sentence = chunks.popSentence(); if (sentence) { this.voice.say(sentence); } return chunks.text; }


Последние штрихи

Даже при всех наших оптимизациях задержка в 3-4 секунды все равно значительна. Мы решили позаботиться об интерфейсе с обратной связью, чтобы избавить пользователя от ощущения, что ответ зависает. Мы рассмотрели несколько подходов:


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


  • Слова-вставки , такие как «Дай мне подумать», «Хм» и т. д., имитируют реальную речь. Мы отвергли этот вариант, поскольку филлеры часто не соответствовали тону ответов модели.


  • Поставьте экран в кабинку. И отображайте различные состояния с помощью анимации.


Мы остановились на последнем варианте с простой веб-страницей, которая опрашивает серверную часть и показывает анимацию в соответствии с текущим состоянием.


Результаты

Наша исповедальная комната с искусственным интеллектом работала четыре дня и привлекла сотни посетителей. Мы потратили всего около 50 долларов на API OpenAI. Взамен мы получили существенные положительные отзывы и ценные впечатления.


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


Кстати, исходники бэкенда доступны на GitHub.