paint-brush
자신만의 AI 고백을 구축하는 방법: LLM에 음성을 추가하는 방법by@slavasobolev
661
661

자신만의 AI 고백을 구축하는 방법: LLM에 음성을 추가하는 방법

Iaroslav Sobolev11m2024/07/14
Read on Terminal Reader

2월 말 발리에서는 유명한 버닝맨의 원칙에 따라 기획된 [Lampu](https://lampu.org/) 축제가 열렸습니다. 우리는 가톨릭 고해성사의 아이디어와 현재 LLM의 역량에 영감을 받았습니다. 우리는 누구나 인공지능과 대화할 수 있는 AI 고백실을 만들었습니다.
featured image - 자신만의 AI 고백을 구축하는 방법: LLM에 음성을 추가하는 방법
Iaroslav Sobolev HackerNoon profile picture
0-item
1-item

OpenAI가 ChatGPT용 고급 음성 모드 출시를 연기 하는 동안 LLM 음성 애플리케이션을 구축하고 대화형 부스에 통합한 방법을 공유하고 싶습니다.

정글에서 AI와 대화하세요

2월 말, 발리에서는 유명한 버닝맨(Burning Man)의 원칙에 따라 준비된 람푸(Lampu) 축제가 열렸습니다. 전통에 따라 참가자들은 자신만의 설치물과 예술품을 만듭니다.


캠프 19:19 의 친구들과 저는 가톨릭 고해소 아이디어와 현재 LLM의 역량에 영감을 받아 누구나 인공 지능과 대화할 수 있는 자체 AI 고해소를 구축하겠다는 아이디어를 생각해 냈습니다.


처음에 우리가 구상한 방법은 다음과 같습니다.

  • 사용자가 부스에 입장하면 새 세션을 시작해야 한다고 판단합니다.


  • 사용자가 질문하면 AI가 듣고 대답합니다. 우리는 모든 사람이 자신의 생각과 경험을 공개적으로 논의할 수 있는 신뢰할 수 있고 사적인 환경을 만들고 싶었습니다.


  • 사용자가 방을 나가면 시스템은 세션을 종료하고 모든 대화 세부정보를 잊어버립니다. 이는 모든 대화를 비공개로 유지하는 데 필요합니다.

개념의 증거

개념을 테스트하고 LLM에 대한 프롬프트로 실험을 시작하기 위해 어느 날 저녁에 순진한 구현을 만들었습니다.

  • 마이크를 들어보세요.
  • STT(Speech-to-Text) 모델을 사용하여 사용자 음성을 인식합니다.
  • LLM을 통해 응답을 생성합니다.
  • TTS(텍스트 음성 변환) 모델을 사용하여 음성 응답을 합성합니다.
  • 사용자에게 응답을 재생합니다.



이 데모를 구현하기 위해 저는 OpenAI의 클라우드 모델인 Whisper , GPT-4TTS 에 전적으로 의존했습니다. 뛰어난 라이브러리 speech_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초 이상입니다. 이는 좋지 않지만 응답 시간을 최적화하는 방법은 여러 가지가 있습니다.


  • 주변 소음 . 우리는 시끄러운 환경에서는 사용자가 말하기를 시작하고 끝내는 시점을 자동으로 감지하기 위해 마이크에만 의존할 수 없다는 사실을 발견했습니다. 문구의 시작과 끝을 인식하는 것( endpointing )은 쉽지 않은 작업입니다. 이를 음악 축제의 시끄러운 환경과 결합하면 개념적으로 다른 접근 방식이 필요하다는 것이 분명해집니다.


  • 실시간 대화를 모방합니다 . 우리는 사용자에게 AI를 방해할 수 있는 기능을 제공하고 싶었습니다. 이를 달성하려면 마이크를 계속 켜 놓아야 합니다. 하지만 이 경우 사용자의 음성을 배경음뿐만 아니라 AI의 음성에서도 분리해야 합니다.


  • 피드백 . 응답 지연으로 인해 때때로 시스템이 정지된 것처럼 보였습니다. 우리는 응답이 처리되는 기간을 사용자에게 알려야 한다는 것을 깨달았습니다.


우리는 적합한 엔지니어링 솔루션이나 제품 솔루션을 찾는 방식으로 이러한 문제를 해결하는 방법을 선택할 수 있었습니다.

부스의 UX를 통해 생각하기

코드를 작성하기 전에 사용자가 부스와 상호 작용하는 방법을 결정해야 했습니다.

  • 과거 대화 기록을 재설정하기 위해 부스에서 새로운 사용자를 감지하는 방법을 결정해야 합니다.


  • 사용자의 말의 시작과 끝을 인식하는 방법과 AI를 방해하려는 경우 어떻게 해야 할까요?


  • AI의 응답이 지연될 때 피드백을 구현하는 방법입니다.


부스 내 새로운 사용자를 감지하기 위해 문 열림 센서, 바닥 무게 센서, 거리 센서, 카메라 + YOLO 모델 등 여러 가지 옵션을 고려했습니다. 등 뒤의 거리 센서는 무게 센서와 달리 문이 충분히 닫히지 않은 경우와 같은 우발적인 트리거를 배제하고 복잡한 설치가 필요하지 않기 때문에 가장 신뢰할 수 있는 것처럼 보였습니다.


대화의 시작과 끝을 인식하는 어려움을 피하기 위해 마이크를 제어할 수 있는 커다란 빨간색 버튼을 추가하기로 결정했습니다. 또한 이 솔루션을 통해 사용자는 언제든지 AI를 중단할 수 있었습니다.


우리는 요청 처리에 대한 피드백을 구현하는 것에 대해 다양한 아이디어를 가지고 있었습니다. 우리는 마이크 듣기, 질문 처리, 응답 등 시스템이 수행하는 작업을 보여주는 화면이 있는 옵션을 결정했습니다.


우리는 또한 오래된 유선 전화를 사용하는 다소 현명한 옵션을 고려했습니다. 사용자가 전화를 받으면 세션이 시작되고 시스템은 사용자가 전화를 끊을 때까지 사용자의 말을 듣습니다. 그러나 우리는 전화 음성보다는 부스에서 사용자가 "응답"하는 것이 더 실제적이라고 판단했습니다.


설치 중 및 축제 중


결국 최종 사용자 흐름은 다음과 같이 나왔습니다.

  • 사용자가 부스로 들어갑니다. 등 뒤에서 거리 센서가 작동하고 우리는 그에게 인사합니다.


  • 사용자는 빨간색 버튼을 눌러 대화를 시작합니다. 버튼을 누르는 동안 마이크를 듣습니다. 사용자가 버튼을 놓으면 요청 처리가 시작되고 이를 화면에 표시됩니다.


  • AI가 답변하는 동안 사용자가 새로운 질문을 하고 싶다면 버튼을 다시 누르면 AI가 즉시 답변을 중단합니다.


  • 사용자가 부스를 떠나면 거리 센서가 다시 작동하고 대화 기록을 지웁니다.

건축학


Arduino는 거리 센서와 빨간색 버튼의 상태를 모니터링합니다. HTTP API를 통해 모든 변경 사항을 백엔드로 전송합니다. 이를 통해 시스템은 사용자가 부스에 입장했는지 또는 부스를 떠났는지 여부와 마이크 청취를 활성화해야 하는지 또는 응답 생성을 시작해야 하는지 여부를 결정할 수 있습니다.


UI는 백엔드로부터 시스템의 현재 상태를 지속적으로 수신하여 사용자에게 표시하는 브라우저에서 열리는 웹 페이지일 뿐입니다.


백엔드는 마이크를 제어하고 필요한 모든 AI 모델과 상호 작용하며 LLM 응답을 음성으로 전달합니다. 여기에는 앱의 핵심 로직이 포함되어 있습니다.

하드웨어

아두이노용 스케치를 코딩하는 방법, 거리센서와 버튼을 제대로 연결하는 방법, 모두 부스에서 조립하는 방법은 별도의 글로 다루겠습니다. 기술적인 세부 사항을 다루지 않고 우리가 얻은 내용을 간략하게 검토해 보겠습니다.


우리는 Wi-Fi 모듈이 내장된 Arduino, 보다 정확하게는 ESP32 모델을 사용했습니다. 마이크로 컨트롤러는 백엔드를 실행하는 노트북과 동일한 Wi-Fi 네트워크에 연결되었습니다.



우리가 사용한 전체 하드웨어 목록:

백엔드

파이프라인의 주요 구성 요소는 STT(Speech-To-Text), LLM 및 TTS(Text-To-Speech)입니다. 각 작업에 대해 로컬과 클라우드를 통해 다양한 모델을 사용할 수 있습니다.



우리는 강력한 GPU를 보유하고 있지 않았기 때문에 클라우드 기반 버전의 모델을 선택하기로 결정했습니다. 이 접근 방식의 약점은 좋은 인터넷 연결이 필요하다는 것입니다. 그럼에도 불구하고 모든 최적화 이후의 상호작용 속도는 페스티벌에서 사용했던 모바일 인터넷에서도 허용 가능한 수준이었습니다.


이제 파이프라인의 각 구성요소를 자세히 살펴보겠습니다.

음성 인식

많은 최신 장치에서는 오랫동안 음성 인식을 지원해 왔습니다. 예를 들어 Apple Speech API는 iOS 및 macOS에서 사용할 수 있고 Web Speech API는 브라우저에서 사용할 수 있습니다.


불행하게도 WhisperDeepgram 에 비해 품질이 매우 떨어지며 자동으로 언어를 감지할 수 없습니다.


처리 시간을 줄이기 위한 가장 좋은 방법은 사용자가 말할 때 실시간으로 음성을 인식하는 것입니다. 구현 방법에 대한 예가 포함된 일부 프로젝트는 다음과 같습니다. 속삭임_스트리밍 , 속삭임.cpp


우리 노트북에서는 이 접근 방식을 사용한 음성 인식 속도가 실시간과는 거리가 먼 것으로 나타났습니다. 여러 번의 실험 끝에 우리는 OpenAI의 클라우드 기반 Whisper 모델을 결정했습니다.

LLM 및 프롬프트 엔지니어링

이전 단계의 Speech To Text 모델 결과는 대화 기록과 함께 LLM에 보내는 텍스트입니다.


LLM을 선택할 때 GPT-3.5를 비교했습니다. GPT-4와 클로드. 핵심 요소는 특정 모델이 아니라 구성이라는 것이 밝혀졌습니다. 궁극적으로 우리는 다른 것보다 답변이 더 마음에 들었던 GPT-4에 정착했습니다.


LLM 모델에 대한 프롬프트 사용자 정의는 별도의 예술 형식이 되었습니다. 필요에 따라 모델을 조정하는 방법에 대한 많은 가이드가 인터넷에 있습니다.



모델이 매력적이고 간결하며 유머러스하게 반응할 수 있도록 프롬프트 및 온도 설정을 광범위하게 실험해야 했습니다.

텍스트 음성 변환

우리는 Text-To-Speech 모델을 사용하여 LLM으로부터 받은 응답을 음성으로 표현하고 이를 사용자에게 재생합니다. 이 단계가 데모 지연의 주요 원인이었습니다.


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초 지연은 여전히 상당합니다. 사용자가 응답이 멈춘다는 느낌을 받지 않도록 UI를 피드백으로 관리하기로 결정했습니다. 우리는 몇 가지 접근 방식을 살펴보았습니다.


  • LED 표시기 . 우리는 유휴, 대기, 듣기, 생각, 말하기의 5가지 상태를 표시해야 했습니다. 그러나 우리는 LED를 사용하여 이해하기 쉬운 방식으로 이를 수행하는 방법을 알 수 없었습니다.


  • "Let me think", "Hmm" 등과 같은 보충 단어는 실제 음성을 모방합니다. 필러가 종종 모델의 응답 톤과 일치하지 않기 때문에 우리는 이 옵션을 거부했습니다.


  • 부스에 스크린을 설치하세요. 그리고 애니메이션으로 다양한 상태를 표시합니다.


우리는 백엔드를 폴링하고 현재 상태에 따라 애니메이션을 표시하는 간단한 웹 페이지를 사용하여 마지막 옵션을 선택했습니다.


결과

우리의 AI 고백실은 4일 동안 운영되었으며 수백 명의 참석자가 모였습니다. 우리는 OpenAI API에 약 50달러를 지출했습니다. 그 대가로 우리는 상당한 긍정적인 피드백과 귀중한 인상을 받았습니다.


이 작은 실험은 제한된 리소스와 까다로운 외부 조건에서도 LLM에 직관적이고 효율적인 음성 인터페이스를 추가하는 것이 가능하다는 것을 보여주었습니다.


그건 그렇고, GitHub에서 사용 가능한 백엔드 소스