使用 ChatGPT 和 WebRTC 与 AI 进行实时对话
Jarvis。Samantha。Joi。HAL。
科幻小说早就梦想着拟人化的AI。有了 GPT、Claude、Bard 以及其他大型语言模型(LLM),这似乎即将成为现实。
虽然我们很喜欢与 ChatGPT 互发短信,但 LiveKit 团队认为,如果能看到并说出它会更有趣。我们恰好致力于开发一套用于构建实时视频和音频应用的开发者工具,这使得构建这个应用变得容易得多。
认识 KITT(Kaleidoscopic, Interconnected Talking Transformer):一个你可以或你的小组可以进行实时对话的AI。KITT 可以做很多很酷的事情,包括:
- 像 Siri、Alexa 或 Google Assistant 一样回答问题
- 对会议讨论内容进行记录或总结
- 说多种语言,甚至充当第三方翻译
本文的其余部分将深入探讨 KITT 的内部工作原理,但如果你想直接看代码,在这里:https://github.com/livekit-examples/kitt
构建 KITT
我们希望客户端尽可能“轻量”。理想情况下,KITT 或任何其他由开发者构建的机器人可以接入 LiveKit 会话并发布视频和/或音频轨道,类似于人类用户分享他们的摄像头或麦克风流。
KITT 还需要从会话中的每个用户那里获取音频流,以便将语音转换为文本,并可能向 GPT 发送提示。我们使用了 LiveKit 的 Go SDK,它封装了 Pion,使我们能够像 WebRTC 客户端一样从后端加入会话。总的来说,应用架构如下所示:
每当一个新的会话开始且第一个用户加入时,我们使用 webhook 让 KITT 也加入。
func (s *LiveGPT) webhookHandler(w http.ResponseWriter, req *http.Request) {
event, err := webhook.ReceiveWebhookEvent(req, s.keyProvider)
if event.Event == webhook.EventParticipantJoined {
// if the GPT participant is not connected, connect it
p, err := ConnectGPTParticipant(s.config.LiveKit.Url, jwt, language, s.sttClient, s.ttsClient, s.gptClient)
}
...
}https://github.com/livekit-examples/kitt/blob/main/lkgpt-service/pkg/service/server.go#L118
现在 KITT 已连接到会话,它需要订阅每个用户的所有音频轨道。
func (p *GPTParticipant) trackPublished(publication *lksdk.RemoteTrackPublication, rp *lksdk.RemoteParticipant) {
if publication.Source() != livekit.TrackSource_MICROPHONE || ... {
return
}
err := publication.SetSubscribed(true)
...
}https://github.com/livekit-examples/kitt/blob/main/lkgpt-service/pkg/service/gptparticipant.go#L174
有了音频流输入,事情变得有趣起来……
优化延迟
我们希望与 KITT 的对话感觉像人与人之间交流,因此我们在编写代码之前首要考虑的是延迟。特别是,用户说话与 KITT 回应之间需要尽可能短的延迟。在有人(OpenAI?)构建音频到音频模型之前,我们的管道中有三个地方可能会引入延迟:1) 语音转文本(STT)2) GPT 3) 文本转语音(TTS)。
STT
我们评估了几种 STT 服务,包括 Google Cloud、DeepGram、Web Speech 和 Whisper。为了让与 KITT 的互动感觉更人性化,我们愿意牺牲转录准确性以换取更低的延迟。DeepGram 的模型似乎有很好的准确性,但在我们的测试中比 Google 的服务慢得多。OpenAI 云托管的 Whisper API 目前不支持流式识别,所以无法使用:虽然 GPT 需要完整的文本提示,但捕获增量转录比发送一个冗长的语音片段更快。
如果每个人都通过 Chrome 访问 KITT,Web Speech API 是一个不错的选择,但它在各个浏览器之间不标准化。有些浏览器使用设备上的模型,这确实比在服务器上处理语音更快,但准确性会不成比例地下降。客户端 STT 也与我们的模块化设计目标(即完全服务器端机器人)相悖。
最终,Google Cloud 的 STT 速度快、准确,并且支持流式识别。在向 STT 服务发送音频时,Google 建议使用 100ms 帧大小以平衡延迟和准确性。在实践中,我们发现 20ms(WebRTC 的默认编码)的帧大小足以满足我们的用例。
您可以在这里查看我们的实现:
https://github.com/livekit-examples/kitt/blob/main/lkgpt-service/pkg/service/transcriber.go
GPT
GPT 接收文本提示并输出文本响应,但每个模型都有不同的特性。
我们选择了 GPT-3.5,因为它侧重于速度,但我们不得不做一些调整。GPT 输出的标记是流式的,与 STT 类似,文本转语音在增量执行时也比从一个大的文本块生成语音更快。然而,与 STT 阶段不同,这是我们管道中的最后阶段,我们不必等待完整的 TTS 输出;我们可以实时将生成的音频片段流式传输给每个用户。为了避免卡顿或不均匀的语音,我们选择按句子划分音频片段。
func (c *ChatStream) Recv() (string, error) {
sb := strings.Builder{}
for {
response, err := c.stream.Recv()
if err != nil {
...
}
delta := response.Choices[0].Delta.Content
sb.WriteString(delta)
if strings.HasSuffix(strings.TrimSpace(delta), ".") {
return sb.String(), nil
}
}
}https://github.com/livekit-examples/kitt/blob/b45f16481495133b12df109899d9ccc3b4e3c2e8/lkgpt-service/pkg/service/completion.go#L86
按句子划分的问题在于我们使用的模型在响应中不够简洁,这会增加 TTS 延迟。为了解决这个问题,我们像这样对 GPT 进行预处理:
stream, err := c.client.CreateChatCompletionStream(ctx, openai.ChatCompletionRequest{
Model: openai.GPT3Dot5Turbo,
Messages: []openai.ChatCompletionMessage{
{
Role: openai.ChatMessageRoleSystem,
Content: "You are a voice assistant in a meeting named KITT, make concise/short answers. " ...,
},
...
},
...
})https://github.com/livekit-examples/kitt/blob/b45f16481495133b12df109899d9ccc3b4e3c2e8/lkgpt-service/pkg/service/completion.go#L45
可能有更好的提示来让 GPT 给出更短的句子,但上述方法在实践中表现良好。从这里开始,将每个句子通过 Google 的 TTS 运行并将音频响应传输给会话中的每个用户就相对简单了。
go func() {
...
resp, err := p.synthesizer.Synthesize(p.ctx, sentence, tmpLang)
...
err = p.gptTrack.QueueReader(bytes.NewReader(resp.AudioContent))
...
}()https://github.com/livekit-examples/kitt/blob/b45f16481495133b12df109899d9ccc3b4e3c2e8/lkgpt-service/pkg/service/gptparticipant.go#L439
我们遵循的优化延迟的一般原则是流式传输所有内容:通过最小化在每个步骤接收、处理和传输数据所需的时间,我们能够将延迟保持在最低限度,并创建无缝的对话体验。
设计客户端界面
有了可以插入任何 LiveKit 会话的 KITT 后端,我们没有构建一个全新的 UI,而是通过使用 LiveKit Meet 为这个项目节省了大量时间。Meet 是一个受 Zoom 启发的示例应用程序,我们构建它是为了向开发者展示如何使用 LiveKit,以及用于内部测试我们的基础设施。在这个演示中,当你开始会议时,KITT 也会自动加入。
如果是 1 对 1 会议(即只有一个人类用户),KITT 会认为你说的任何话都是针对它的,并做出适当回应。如果会议中有多个用户,说“KITT”或“Hey KITT”会让 KITT 知道你接下来的提示是针对它的——我们还会播放声音让你知道 KITT 正在听。
借鉴 Siri 和 Google Assistant 等助手界面,每当你与 KITT 交谈时,我们会显示你提示的实时转录,这有助于你了解 KITT 接收到的输入并将其回应情境化。

对于 KITT 的视觉形象,我们最终希望在后端动态生成视频帧,但对于这个初始版本,我们选择构建一个客户端 React 组件。KITT 会根据对话状态以及它是否处于参与状态,在几种不同状态之间循环。
实时转录和 KITT 的状态都使用 LiveKit 的数据消息传输给每个用户。
将所有内容整合在一起
当所有功能端到端工作时,结果非常神奇。✨
接下来是什么?
以下是我们希望探索作为 KITT 实现改进的一些领域——如果你有兴趣为其中任何一项做出贡献,我们接受 PR!🙂
STT
Google 的 STT 速度快且准确,但有两个缺点:1) 这是一个外部服务调用 2) 价格不便宜。我们想探索自己运行最小(即最快)的 Whisper 模型,看看延迟是否能显著降低。Whisper 的局限性在于它的语言支持不如 Google 广泛。
TTS
Google 的 TTS 也很快,但声音听起来有点机器人。我们探索了 Tortoise,它听起来很棒,但生成一个句子需要大约 20 秒!值得测试其他实时 TTS 模型,例如 Rime,甚至是支持自定义声音的模型,这将对最终用户来说很有趣。
GPT-4 和其他模型
GPT-4 是一个更强大的模型,能够产生更像人类的响应,但代价是速度。有趣的是,预先提示它(使其格外简洁)是否能帮助减少增加的延迟。此外,还有像 Claude 这样非常快的模型,或者可能在本地运行 LLaMA 或 Alpaca,这可能会在不显著影响响应质量的情况下实现更低的延迟。
更多更好的提示
在这个演示中,我们几乎没有触及预先提示 GPT 能做的事情的表面。我们添加了让每个用户指定其口语语言的功能,并将语言代码(例如 en-US)前置到每个用户发起的提示中。这使得 KITT 能够以适当的语言回应每个用户,甚至充当两个用户之间的实时翻译。
对于每个用户查询,我们还向 GPT 传递整个对话历史(例如 russ: ...\ntheo: ...),以便 KITT 可以回应或引用依赖历史上下文的任何提示。
进一步深入的一些想法包括将会话框定为会议,包括来自电子邮件或日历条目的任何笔记,并向历史记录添加实时事件,例如 theo left the meeting at <timestamp> 或实时聊天消息。
头像
为 KITT 或其他 AI 提供富有表现力(也许更像人)的视觉表示将完全改变与之互动的感觉。后端棘手的部分是建立一个管道,该管道可以接收音频流,支持合成/动画/效果,并输出视频帧。一个选择是像我们使用 LiveKit Egress 那样录制浏览器实例。另一种可能性是使用 Unity 或 Unreal。
视频处理
现在我们没有对用户的视频流做任何处理。虽然转录有助于可访问性,但想象一下运行一个单独的模型来执行 ASL 识别!其他事情,如情感分析或场景理解,也可以为 GPT 提供额外的上下文。
屏幕共享
一些 GPT 响应包括多媒体,如视频、图像或代码片段。一个很棒的功能是发起屏幕共享或向 LiveKit Meet 添加某种类型的画布,KITT 可以使用它向用户显示这些类型的资产。
构建 KITT 真是太有趣了,与它们的第一次对话让我们起鸡皮疙瘩。在这个基础上肯定有可能构建一些独立的产品。请将其视为一个开放邀请,带上我们的代码并用它构建一些神奇的东西。如果你在此过程中有任何问题,想讨论想法,或者只是分享你构建的东西,请在 LiveKit Community Slack 中联系我们!
🤖