OpenAI가 ChatGPT용 고급 음성 모드 출시를 연기 하는 동안 LLM 음성 애플리케이션을 구축하고 대화형 부스에 통합한 방법을 공유하고 싶습니다.
2월 말, 발리에서는 유명한 버닝맨(Burning Man)의 원칙에 따라 준비된 람푸(Lampu) 축제가 열렸습니다. 전통에 따라 참가자들은 자신만의 설치물과 예술품을 만듭니다.
캠프 19:19 의 친구들과 저는 가톨릭 고해소 아이디어와 현재 LLM의 역량에 영감을 받아 누구나 인공 지능과 대화할 수 있는 자체 AI 고해소를 구축하겠다는 아이디어를 생각해 냈습니다.
처음에 우리가 구상한 방법은 다음과 같습니다.
개념을 테스트하고 LLM에 대한 프롬프트로 실험을 시작하기 위해 어느 날 저녁에 순진한 구현을 만들었습니다.
이 데모를 구현하기 위해 저는 OpenAI의 클라우드 모델인 Whisper , GPT-4 및 TTS 에 전적으로 의존했습니다. 뛰어난 라이브러리 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())
우리가 해결해야 할 문제는 이 데모의 첫 번째 테스트 이후 즉시 명백해졌습니다.
우리는 적합한 엔지니어링 솔루션이나 제품 솔루션을 찾는 방식으로 이러한 문제를 해결하는 방법을 선택할 수 있었습니다.
코드를 작성하기 전에 사용자가 부스와 상호 작용하는 방법을 결정해야 했습니다.
부스 내 새로운 사용자를 감지하기 위해 문 열림 센서, 바닥 무게 센서, 거리 센서, 카메라 + YOLO 모델 등 여러 가지 옵션을 고려했습니다. 등 뒤의 거리 센서는 무게 센서와 달리 문이 충분히 닫히지 않은 경우와 같은 우발적인 트리거를 배제하고 복잡한 설치가 필요하지 않기 때문에 가장 신뢰할 수 있는 것처럼 보였습니다.
대화의 시작과 끝을 인식하는 어려움을 피하기 위해 마이크를 제어할 수 있는 커다란 빨간색 버튼을 추가하기로 결정했습니다. 또한 이 솔루션을 통해 사용자는 언제든지 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는 브라우저에서 사용할 수 있습니다.
불행하게도 Whisper 나 Deepgram 에 비해 품질이 매우 떨어지며 자동으로 언어를 감지할 수 없습니다.
처리 시간을 줄이기 위한 가장 좋은 방법은 사용자가 말할 때 실시간으로 음성을 인식하는 것입니다. 구현 방법에 대한 예가 포함된 일부 프로젝트는 다음과 같습니다.
우리 노트북에서는 이 접근 방식을 사용한 음성 인식 속도가 실시간과는 거리가 먼 것으로 나타났습니다. 여러 번의 실험 끝에 우리는 OpenAI의 클라우드 기반 Whisper 모델을 결정했습니다.
이전 단계의 Speech To Text 모델 결과는 대화 기록과 함께 LLM에 보내는 텍스트입니다.
LLM을 선택할 때 GPT-3.5를 비교했습니다. GPT-4와 클로드. 핵심 요소는 특정 모델이 아니라 구성이라는 것이 밝혀졌습니다. 궁극적으로 우리는 다른 것보다 답변이 더 마음에 들었던 GPT-4에 정착했습니다.
LLM 모델에 대한 프롬프트 사용자 정의는 별도의 예술 형식이 되었습니다. 필요에 따라 모델을 조정하는 방법에 대한 많은 가이드가 인터넷에 있습니다.
모델이 매력적이고 간결하며 유머러스하게 반응할 수 있도록 프롬프트 및 온도 설정을 광범위하게 실험해야 했습니다.
우리는 Text-To-Speech 모델을 사용하여 LLM으로부터 받은 응답을 음성으로 표현하고 이를 사용자에게 재생합니다. 이 단계가 데모 지연의 주요 원인이었습니다.
LLM은 응답하는 데 꽤 오랜 시간이 걸립니다. 그러나 스트리밍 모드(토큰별)에서 응답 생성을 지원합니다. 이 기능을 사용하면 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를 피드백으로 관리하기로 결정했습니다. 우리는 몇 가지 접근 방식을 살펴보았습니다.
우리는 백엔드를 폴링하고 현재 상태에 따라 애니메이션을 표시하는 간단한 웹 페이지를 사용하여 마지막 옵션을 선택했습니다.
우리의 AI 고백실은 4일 동안 운영되었으며 수백 명의 참석자가 모였습니다. 우리는 OpenAI API에 약 50달러를 지출했습니다. 그 대가로 우리는 상당한 긍정적인 피드백과 귀중한 인상을 받았습니다.
이 작은 실험은 제한된 리소스와 까다로운 외부 조건에서도 LLM에 직관적이고 효율적인 음성 인터페이스를 추가하는 것이 가능하다는 것을 보여주었습니다.
그건 그렇고, GitHub에서 사용 가능한 백엔드 소스