虽然 OpenAI推迟了ChatGPT 高级语音模式的发布,但我想分享我们如何构建 LLM 语音应用程序并将其集成到交互式展台中。
2 月底,巴厘岛举办了Lampu节,该节日按照著名的火人节的原则组织。根据该节日的传统,参与者可以制作自己的装置和艺术品。
我和来自 19:19 营的朋友们受到天主教忏悔室的理念和当前法学硕士的能力的启发,提出了建立我们自己的人工智能忏悔室的想法,任何人都可以与人工智能交谈。
我们最初的设想是这样的:
为了测试这个概念,并开始尝试 LLM 的提示,我在一个晚上创建了一个简单的实现:
为了实现这个演示,我完全依赖于 OpenAI 的云模型: Whisper 、 GPT-4和TTS 。得益于优秀的语音识别库,我只用了几十行代码就构建了这个演示。
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 将所有更改发送到我们的后端,这使系统能够确定用户是否已进入或离开隔间,以及是否需要激活麦克风监听或开始生成响应。
Web UI只是一个在浏览器中打开的网页,它不断从后端接收系统当前状态并将其显示给用户。
后端控制麦克风、与所有必要的 AI 模型交互,并发出 LLM 响应的声音。它包含应用程序的核心逻辑。
如何为 Arduino 编写草图、正确连接距离传感器和按钮,以及在展台上组装所有部件,这是另一篇文章的主题。让我们简要回顾一下我们得到的内容,而不涉及技术细节。
我们使用了 Arduino,更准确地说是内置 Wi-Fi 模块的ESP32型号。微控制器与运行后端的笔记本电脑连接到同一个 Wi-Fi 网络。
我们使用的硬件完整列表:
管道的主要组件是语音转文本 (STT)、LLM 和文本转语音 (TTS)。对于每项任务,本地和云端都有许多不同的模型可用。
由于手头没有强大的 GPU,我们决定选择基于云的模型版本。这种方法的缺点是需要良好的互联网连接。尽管如此,经过所有优化后,即使在节日期间使用移动互联网,交互速度还是可以接受的。
现在,让我们仔细看看管道的每个组件。
许多现代设备早已支持语音识别。例如, Apple Speech API适用于 iOS 和 macOS, Web Speech API适用于浏览器。
不幸的是,它们的质量远不如Whisper或Deepgram ,并且无法自动检测语言。
为了减少处理时间,最好的选择是在用户说话时实时识别语音。以下是一些项目及其实现示例:
在我们的笔记本电脑上,使用这种方法的语音识别速度远远达不到实时水平。经过多次试验,我们决定采用 OpenAI 的基于云的 Whisper 模型。
上一步中的语音转文本模型的结果是我们与对话历史记录一起发送给 LLM 的文本。
在选择法学硕士时,我们比较了 GPT-3.5、GPT-4 和 Claude。结果表明,关键因素不是具体模型,而是其配置。最终,我们选择了 GPT-4,我们比其他的都更喜欢它的答案。
LLM 模型提示的定制已成为一种独立的艺术形式。互联网上有许多关于如何根据需要调整模型的指南:
我们必须对提示和温度设置进行广泛的实验,以使模型做出引人入胜、简洁而幽默的反应。
我们使用文本转语音模型将从 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 告白室持续了四天,吸引了数百名参与者。我们仅花费了大约 50 美元购买 OpenAI API。作为回报,我们收到了大量积极反馈和宝贵印象。
这个小实验表明,即使在资源有限和外部条件具有挑战性的情况下,也可以为 LLM 添加直观、高效的语音界面。
顺便说一下, GitHub 上提供的后端源代码