WheatField
WheatField

如何使用 whisper、llama3 和 edge-tts 做一个口语陪练

August 27, 20241865 words, 10 min read
Authors

在 ChatGPT 提供语音聊天模式后,笔者曾经拿来当作口语陪练使用了一段时间,体验还算不错,但也遇到了一些问题:

  • 首先是回复太书面化,冗长且正式,不够口语化,感觉不像是在与人聊天,而更像是在法庭上与律师进行最后的证词对练。
  • Prompt 难调,我的一个需求是帮我纠正口语中的非标准用法,但设定的 prompt 经常失效,ChatGPT 一个人在那自说自话、孤芳自赏,把用户晾在一边。
  • 网络中断,聊着聊着就没有声音了。当然这点不怪 ChatGPT,身处“局域网”,经常断线是难免的事情。

近两年 LLM 发展迅猛,文本生成、语言模型都取得了不错的进展,本着“有轮堪造直须造、莫待无轮空叹息”的精神,笔者决定自己实现一个口语陪练机器人,除了基本的对话功能之外,还得解决以上 ChatGPT 使用时遇到的几个问题。

动手之前的思路是:

  1. 录音并用 whisper 将语音转换为文本(speech-to-text, stt)
  2. 设定合适的 system prompt,对话使用 gpt-4o 来完成(chat)
  3. 用 OpenAI 的 whisper 或者 Azure tts 将文本转换为语音(text-to-speech, tts)

这个思路其实跟笔者之前做过一个半成品语音转写项目 stream-whisper 比较类似,本质上都是 stt + generation + tts。stream-whisper 使用 PyAudio 进行录音,用部署在 Kaggle 上的 FastWhisper 进行语音转写,本地与服务端通过 redis 进行数据同步,文本生成调用了 OpenAI 的 GPT,再结合 webrtcvad 进行静音检测,截取一段一段的语音进行处理,可以实现不间断连续对话。

这个项目有几点局限。1)纯 Python 实现,没有客户端,只能在终端下运行,日常使用起来不太方便,要练习总不能每次都抱着电脑吧;2)没有实现 tts 这一步,因为前面转写及文本生成就已经有很高的延迟了,再加上 tts,延迟只会更高。

得益于近来 LLM 效果越来越好,推理速度越来越快,借助像 Groqcerebras.ai 这样的大模型 API 平台,所有需要 LLM 参与的功能都可以通过低延迟的 API 调用完成。所以这次考虑结合 NextJS 开发一个 web 版本,随时随地都可以使用。配合最近暴火的 cursor 编辑器,经过两天的训话与拉扯,终于实现了一个极简可用版本。

技术栈

这个项目中多数功能、服务都是基于免费资源搭建的,成本几乎可以忽略不计,感谢 cloudflare、vercel 等云平台及 upstash、deepgram、groq 等模型提供商。项目使用到的服务、模型及开源库如下:

  • 语音转写,项目中使用了 whisper-large-v3 和 distil-whisper-large-v3-en 两个模型,前者速度稍慢,但支持多种语言,后者仅支持英文,但速度更快。
  • 聊天,主要以 llama3-8b 为主,速度快,英文效果好。
  • 语音合成
    • edge-tts(基于微软 Edge tts 的开源项目),为了减少依赖安装,笔者将 edge-tts 单独做成了一个服务 oopstts 部署在 Vercel 上,并提供了一个可公开访问使用的 API 接口。
    • deepgram tts,当前注册即送 200 美元的免费额度,足够使用很久。
  • 历史对话存储:upstash redis。存储可选方案比较多,按理说更标准的做法是以结构化形式存储在数据库中,但为了快速实现,暂时使用了 redis。
  • RAG 向量存储及检索:upstash vector
  • 服务部署:主功能界面部署在 cloudflare pages,RAG 存储及检索部署在 Vercel 上(Python 版本 upstash 更方便),llama 中转、tts 中转服务也都部署在 Vercel 上。
  • 音频数据存储:backblaze 的免费 10G 空间,使用 cloudflare 做 CDN,以提高局域网内部可用性。
实现流程

体验如何

操作流程是手动式触发录音及停止功能,录音完成后就开始转写、生成、音频合成与播放。 得益于 Groq 的快速推理速度,stt 与生成速度都非常快。 实际体验下来,每轮对话在问与答之间有 1~2 秒左右的延迟,且主要集中在网络请求与 RAG 检索上,deepgram 的 tts 也会偶发性的拖一下后腿。

实现过程中笔者也尝试实现了不间断连续对话功能,但效果不佳,因为笔者经常被 bot 问的哑口无言,口语说的结结巴巴,通常一卡壳就是好几秒,基于静音检测的机制很容易产生非常零碎的对话,体验并不好,笔者在使用 ChatGPT 语音聊天时也时常会遇到这样的问题。

语音转写

Groq 的 whisper API 速度非常快,Large-v3 版本官方数据虽然有 9.3% 的错误率,实际体验下来,感觉比这个还要低。

文本生成

文本生成上一开始考虑的使用 OpenAI 的 GPT-4o,作为连续霸榜的明星模型,GPT-4o 在生成质量、速度上要优于市场上多数模型,但经过测试后笔者发现,如果仅用作英文对话,Llama3-8B 的模型已经足够好了,对指令的遵循能力、上下文理解能力都还不错,而且速度更快。

语音合成

语音合成上花了一段时间折腾。一开始考虑的方案是使用 elevenlabs 的 API,但价格太贵,而且有字数限制。 后来调研发现 edge 的 tts 效果还不错,发音虽略显生硬,但速度快且有开源工具包。

音频自动播放

合成的音频是通过 backblaze 的 B2 云存储来存储的,结合 cloudflare 的 CDN 来加速。因为笔者跟 sonnet-3.5 对 NextJS 多媒体处理这一块都比较陌生,在实现音频自动播放这一步时,踩了一些坑。 目前的实现思路如下:

  1. 将语音合成的结果存储在 audioUrl 字段中
  2. 使用 useEffect 监听 messages 的变化,当有新消息时,将 audioUrl 赋值给 audioRef,并自动播放
useEffect(() => {
  const lastMessage = messages[messages.length - 1]
  audioRef.current.src = lastMessage.audioUrl
  audioRef.current.play().catch((e) => console.error('Audio playback failed:', e))
}, [messages])

后续优化

这两天在开发过程中一边体验一边优化,感觉做为一个简单的口语陪练,目前 bot 的水平已经足够使用了,当然也有一些可以优化的地方:

  1. 长期记忆问题。虽然使用了 RAG,但 Llama3 输入上下文有限,且仅召回了有限的内容,随着对话轮数增加,历史数据会越来越多,如何找到最相关的历史数据,并结合上下文一起生成回复,是接下来需要优化的方向。
  2. 对话内容优化。
  • 目前在会话格式上表现不错,都是一些日常简短对话,但有时候 bot 会沉迷于跟用户聊天,忘记纠正口语中的非标准用法。
  • 需要一个更懂口语的模型,或者在 prompt 中加入更多口语的例子,让模型在生成时参考。