虽然 OpenAI ChatGPT 高级语音模式的发布,但我想分享我们如何构建 LLM 语音应用程序并将其集成到交互式展台中。 推迟了 与丛林中的人工智能交谈 2 月底,巴厘岛举办了 节,该节日按照著名的火人节的原则组织。根据该节日的传统,参与者可以制作自己的装置和艺术品。 Lampu 我和 的朋友们受到天主教忏悔室的理念和当前法学硕士的能力的启发,提出了建立我们自己的人工智能忏悔室的想法,任何人都可以与人工智能交谈。 来自 19:19 营 我们最初的设想是这样的: 当用户进入展位时,我们确定需要开始新的会话。 用户提出问题,人工智能倾听并回答。我们希望创造一个信任且私密的环境,让每个人都可以公开讨论自己的想法和经历。 当用户离开房间时,系统将结束会话并忘记所有对话细节。这对于保持所有对话的私密性是必要的。 概念验证 为了测试这个概念,并开始尝试 LLM 的提示,我在一个晚上创建了一个简单的实现: 听麦克风。 使用 模型识别用户语音。 语音到文本 (STT) 通过 生成响应。 LLM 使用 模型合成语音响应。 文本到语音 (TTS) 向用户回放响应。 为了实现这个演示,我完全依赖于 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()) 在对该演示进行首次测试后,我们必须解决的问题立即显现出来: 。在简单的实现中,用户提问和响应之间的延迟为 7-8 秒或更长时间。这并不好,但显然有很多方法可以优化响应时间。 响应延迟 。我们发现,在嘈杂的环境中,我们无法依靠麦克风自动检测用户何时开始和结束说话。识别短语的开始和结束( )并非易事。再加上音乐节的嘈杂环境,显然需要一种概念上不同的方法。 环境噪音 结束语 。我们希望让用户能够打断人工智能。为了实现这一点,我们必须保持麦克风开启。但在这种情况下,我们不仅必须将用户的声音与背景声音分开,还必须将用户的声音与人工智能的声音分开。 模拟现场对话 。由于响应延迟,我们有时感觉系统冻结了。我们意识到需要告知用户响应需要多长时间处理 反馈 我们可以选择如何解决这些问题:寻找合适的工程或产品解决方案。 思考展台的用户体验 在我们开始编码之前,我们必须决定用户如何与展台互动: 我们应该决定如何检测展位中的新用户来重置过去的对话历史。 如何识别用户语音的开始和结束,以及如果他们想打断AI该怎么办。 当AI响应延迟时如何实现反馈。 为了检测亭内是否有新用户,我们考虑了几种方案:开门传感器、地板重量传感器、距离传感器以及摄像头 + YOLO 模型。我们认为,背后的距离传感器最可靠,因为它可以排除意外触发,例如门关得不够紧,而且与重量传感器不同,它不需要复杂的安装。 为了避免识别对话开始和结束的难题,我们决定添加一个大红色按钮来控制麦克风。此解决方案还允许用户随时中断 AI。 对于在处理请求时提供反馈,我们有很多不同的想法。我们决定采用一种方案,即在屏幕上显示系统正在做什么:听麦克风、处理问题或回答问题。 我们还考虑过一个相当聪明的选择,那就是使用老式固定电话。当用户拿起电话时,会话就开始了,系统会一直听用户说话,直到用户挂断电话。然而,我们认为,当用户通过电话亭“接听”电话而不是通过电话中的声音接听电话时,会更真实。 最终,最终的用户流程是这样的: 一名用户走进一个展台。他背后的距离传感器被触发,我们向他打招呼。 用户按下红色按钮开始对话。按下按钮时,我们会监听麦克风的声音。当用户松开按钮时,我们会开始处理请求并将其显示在屏幕上。 如果用户在AI回答时想要提出新的问题,可以再次按下按钮,AI就会立即停止回答。 当用户离开展位时,距离传感器再次触发,我们清除对话历史记录。 建筑学 监控距离传感器和红色按钮的状态。它通过 HTTP API 将所有更改发送到我们的后端,这使系统能够确定用户是否已进入或离开隔间,以及是否需要激活麦克风监听或开始生成响应。 Arduino 只是一个在浏览器中打开的网页,它不断从后端接收系统当前状态并将其显示给用户。 Web UI 控制麦克风、与所有必要的 AI 模型交互,并发出 LLM 响应的声音。它包含应用程序的核心逻辑。 后端 硬件 如何为 Arduino 编写草图、正确连接距离传感器和按钮,以及在展台上组装所有部件,这是另一篇文章的主题。让我们简要回顾一下我们得到的内容,而不涉及技术细节。 我们使用了 Arduino,更准确地说是内置 Wi-Fi 模块的 型号。微控制器与运行后端的笔记本电脑连接到同一个 Wi-Fi 网络。 ESP32 我们使用的硬件完整列表: — 我们从朋友那里借来的 :) Skype 亭 美元 ESP32——29 ——5美元 大红色按钮 ——10美元 接近传感器 旧款 Mackbook Air — 我们有一台。 后端 管道的主要组件是语音转文本 (STT)、LLM 和文本转语音 (TTS)。对于每项任务,本地和云端都有许多不同的模型可用。 由于手头没有强大的 GPU,我们决定选择基于云的模型版本。这种方法的缺点是需要良好的互联网连接。尽管如此,经过所有优化后,即使在节日期间使用移动互联网,交互速度还是可以接受的。 现在,让我们仔细看看管道的每个组件。 语音识别 许多现代设备早已支持语音识别。例如, 适用于 iOS 和 macOS, 适用于浏览器。 Apple Speech API Web Speech API 不幸的是,它们的质量远不如 或 ,并且无法自动检测语言。 Whisper Deepgram 为了减少处理时间,最好的选择是在用户说话时实时识别语音。以下是一些项目及其实现示例: , whisper_streaming 耳语.cpp 在我们的笔记本电脑上,使用这种方法的语音识别速度远远达不到实时水平。经过多次试验,我们决定采用 OpenAI 的基于云的 Whisper 模型。 法学硕士和快速工程 上一步中的语音转文本模型的结果是我们与对话历史记录一起发送给 LLM 的文本。 在选择法学硕士时,我们比较了 GPT-3.5、GPT-4 和 Claude。结果表明,关键因素不是具体模型,而是其配置。最终,我们选择了 GPT-4,我们比其他的都更喜欢它的答案。 LLM 模型提示的定制已成为一种独立的艺术形式。互联网上有许多关于如何根据需要调整模型的指南: OpenAI Prompt 工程指南 人为快速工程 及时工程指南 我们必须对提示和 设置进行广泛的实验,以使模型做出引人入胜、简洁而幽默的反应。 温度 文字转语音 我们使用文本转语音模型将从 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 以易于理解的方式实现这一点。 LED 指示灯 ,例如“让我想想”、“嗯”等,模仿真实生活中的言语。我们拒绝了此选项,因为填充词通常与模型的响应语气不匹配。 填充词 在展台上放置 。并用动画显示不同的状态。 一块屏幕 我们选择了最后一个选项,即一个简单的网页,该网页轮询后端并根据当前状态显示动画。 结果 我们的 AI 告白室持续了四天,吸引了数百名参与者。我们仅花费了大约 50 美元购买 OpenAI API。作为回报,我们收到了大量积极反馈和宝贵印象。 这个小实验表明,即使在资源有限和外部条件具有挑战性的情况下,也可以为 LLM 添加直观、高效的语音界面。 顺便说一下, 后端源代码 GitHub 上提供的