图片来源:Mariia Shalabaieva 提供的照片, 来自 Unsplash
我已经参加了整整十年的GeekCon(没错,就是极客们的大会 🙂)——一个为期周末的黑客马拉松制作马拉松,在这期间所有的项目都必须是无用且纯娱乐性质的。而今年有一个令人兴奋的新花样:所有的项目都必须加入某种形式的人工智能。
我的小组项目是一个语音转文字再转语音的游戏,下面是它的运作方式:用户选择一个想要交谈的角色,然后可以说出他们想对角色说的话。语音输入会被转换成文字并传送给ChatGPT,ChatGPT会像角色一样回复。最后,这个回复会通过语音合成技术说出来。
现在游戏已经上线,给大家带来了欢笑和乐趣,我写了一份指南,希望能帮助你制作出自己的游戏。在这篇文章里,我也会分享一下在黑客马拉松期间的一些思考和决定。
想看完整代码吗?可以在这里查看。
程序流程一旦服务器开始运行,应用将会提示用户选择想要对话的角色并开始与所选角色交谈。每当他们想大声说话时,只需按住键盘上的一个键即可。说完话并松开键后,他们的录音将由 [Whisper](https://platform.openai.com/docs/guides/speech-to-text/quickstart)
(一个由 [OpenAI](https://platform.openai.com/docs/introduction/overview)
提供的语音转文本模型)转录,转录内容将发送到 [ChatGPT](https://platform.openai.com/docs/guides/gpt/chat-completions-api)
获取回复。回复将由一个文本转语音工具朗读出来,用户就能听到。
需要特别注意的是,pyttsx3
库不支持MacOS,该项目是在Windows操作系统上开发的,并且使用了pyttsx3
库,该库不兼容M1/M2芯片。因为pyttsx3
不支持MacOS,建议用户寻找其他与macOS环境兼容的文本转语音库。
我使用了两个OpenAI
模型:Whisper
用于语音转文字的转录功能,以及ChatGPT
API根据用户的输入为他们选择的角色生成回复。虽然这样做会花费一些钱,但价格非常便宜,我个人的费用至今仍低于1美元。一开始,我存了5美元作为预付款,而且这笔预付款一年内不会过期。
我写这篇文章并没有从OpenAI
那里得到任何报酬或好处。
一旦你获得了OpenAI
的API密钥,将其设置为环境变量,在调用API时使用。确保不要将密钥推送到代码库或其他公共位置,也不要随意分享。
我们使用了Whisper
来实现语音转文字的功能,这是一个来自OpenAI
的模型。
下面是用于转录的函数代码片段:
async def get_transcript(audio_file_path: str,
text_to_draw_while_waiting: str) -> Optional[str]: # 获取转录音频的文字内容
openai.api_key = os.environ.get("OPENAI_API_KEY")
audio_file = open(audio_file_path, "rb")
transcript = None
async def transcribe_audio() -> None:
nonlocal transcript
try:
response = openai.Audio.transcribe(
model="whisper-1", file=audio_file, language="en")
transcript = response.get("text")
except Exception as e:
print(f"错误: {e}") # 错误处理,打印错误信息
draw_thread = Thread(target=等待转录时显示文本(text_to_draw_while_waiting)) # 等待转录时显示文本
draw_thread.start()
transcription_task = asyncio.create_task(transcribe_audio()) # 创建一个异步任务来转录音频
await transcription_task
if transcript is None:
print("转录内容在指定时间内无法获取。") # 如果转录内容在指定时间内无法获取
# 返回转录内容
return transcript
此功能被标记为异步(async),因为 API 调用可能需要一些时间才能返回响应,我们等待它,以确保程序不会在响应返回之前继续执行。
正如你所看到的,get_transcript
函数也调用了 print_text_while_waiting_for_transcription
函数。为什么呢?因为获取转写需要花费较长时间,我们希望用户知道程序正在积极处理他们的请求,而不是卡顿或没有反应。因此,这段文本会在用户等待下一步的过程中逐渐显示出来。
将演讲转录成文字之后,我们要么直接使用这个转录文本,要么尝试将其与现有的文本进行比较。
比较用例包括:从预定义的选项列表中选择一个选项,决定是否继续玩,如果选择继续,则决定是换一个新选项还是继续用现在的。
我们想将用户的语音输入转写与我们列表里的选项进行比较,因此决定使用FuzzyWuzzy
库来进行字符串匹配工作。
这就能让从列表中选择最合适的选项,只要匹配得分超过了设定的门槛。
这里是我们代码的一个片段
def 从转录中检测选择的选项(transcript: str, options: List[str]) -> str:
最佳匹配得分 = 0
最佳匹配项 = ""
# 从选项列表中遍历每个选项
for 选项 in options:
得分 = fuzz.token_set_ratio(transcript.lower(), 选项.lower())
# 如果得分大于最佳匹配得分,则更新最佳匹配得分和最佳匹配项
if 得分 > 最佳匹配得分:
最佳匹配得分 = 得分
最佳匹配项 = 选项
# 如果最佳匹配得分大于等于70,则返回最佳匹配项
if 最佳匹配得分 >= 70:
return 最佳匹配项
else:
# 如果最佳匹配得分小于70,则返回空字符串
return ""
想了解更多的话,可以去看看我写的一篇关于FuzzyWuzzy
库及其功能特性的文章,可以在这里找到这里。
一旦我们有了转写文本,就可发给 ChatGPT
获取回复。
对于每个 ChatGPT
的请求,我们都添加了一个简短有趣的回答的提示。此外,让 ChatGPT
假装成某个角色。
因此我们的函数看起来像这样:
def get_gpt_response(transcript: str, chosen_figure: str) -> str:
system_instructions = get_system_instructions(chosen_figure)
try:
return make_openai_request(
system_instructions=system_instructions,
user_question=transcript).choices[0].message["content"]
except Exception as e:
logging.error(f"无法从ChatGPT获取响应。错误 {str(e)}")
raise e
系统指令如下:
def get_system_instructions(figure: str) -> str:
return f"你的回答要有趣且简短,你是:{figure},"
文字转语音功能
在文本转语音这一部分,我们选择了一个名为 pyttsx3
的 Python 库。这个选择不仅实现简单,而且还有一些额外好处。它是免费的,提供了两种声音选择:男声和女声,还允许你设定每分钟的语速。
当玩家启动游戏时,他们会在预设的角色列表中选择一个角色。如果我们无法找到他们选择的角色,我们会随机选择一个“备用角色”。每个角色都关联着一个性别,因此我们的语音合成功能也会收到与所选性别对应的语音ID(用于匹配性别对应的语音)。
这就是我们的文本转语音功能。
def 文本转语音(text: str, gender: str = 'female' if Gender.FEMALE.value == 'female' else 'male') -> None:
engine = pyttsx3.init()
engine.setProperty("rate", WORDS_PER_MINUTE_RATE) # 每分钟单词数的速率
voices = engine.getProperty("voices")
voice_id = voices[1].id if gender == "male" else voices[0].id
engine.setProperty("voice", voice_id)
engine.say(text)
engine.runAndWait()
主要流程
现在我们差不多已经准备好应用程序的所有部分了,是时候深入游戏玩法了!主要流程如下所示。你可能已经注意到一些我们还没有详细讨论的功能(例如 choose_figure
,play_round
),但你可以通过查看仓库来查看完整的代码。这些高层次的功能最终都会和我们之前提到的内部功能联系起来。
这里有一段游戏的主要流程:
这里有一段游戏的主要流程:
import asyncio
from src.handle_transcript import text_to_speech
from src.main_flow_helpers import choose_figure, start, play_round, is_another_round
def farewell() -> None:
farewell_message = "和你在一起真是太好了,希望很快再见到你!"
print(f"\n{farewell_message}")
text_to_speech(farewell_message)
async def get_round_settings(figure: str) -> dict:
new_round_choice = await is_another_round()
if new_round_choice == "新角色":
return {"figure": "", "another_round": True}
elif new_round_choice == "不":
return {"figure": "", "another_round": False}
elif new_round_choice == "是的":
return {"figure": figure, "another_round": True}
async def main():
start()
another_round = True
figure = ""
while True:
if not figure:
figure = await choose_figure()
while another_round:
await play_round(chosen_figure=figure)
user_choices = await get_round_settings(figure)
figure, another_round = user_choices.get("figure"), user_choices.get("another_round")
if not figure:
break
if another_round is False:
farewell()
break
if __name__ == "__main__":
asyncio.run(main())
未走过的路
在黑客马拉松期间,我们有几个想法未能实现。这要么是因为在那个周末我们找不到满意的 API,要么是因为时间不够我们无法开发某些功能。以下是这个项目未走的路,
使回应的语气贴近所选角色的“典型”语气想象如果用户选择与史瑞克、特朗普或奥普拉·温弗瑞聊天。我们希望我们的文字转语音库或API能够用与所选人物匹配的嗓音来回应。然而,在那次黑客马拉松期间,我们还没有找到提供这种功能且价格合理的库或API。如果您有任何建议,欢迎告诉我们哦 =)
让用户跟自己聊一聊另一个有趣的想法是让用户提供一段自己的语音样本。我们会用这个样本来训练模型,并让ChatGPT生成的所有回答都用用户的语音朗读出来。在这种设定下,用户可以选择回答的语气(比如肯定和支持、讽刺、愤怒等),但声音会非常接近用户本人的声音。然而,我们没有在黑客马拉松期间找到支持这一功能的API。
给我们的应用加入前端我们的最初计划是在应用程序中加入一个前端组件。然而,在最后一刻,小组参与人数发生了变化,我们决定优先处理后端开发。因此,应用程序现在在命令行界面(CLI)上运行,目前没有前端。
我们想到的更多改进现在最让我头疼的是延迟问题。
流程中有几个环节延迟较高,我认为这对用户体验有些影响。例如:从输入音频到收到转录的时间,以及用户点击按钮到系统开始录音之间的时间。因此,如果用户一按键就开始说话,至少会有一秒的音频没被记录,因为存在这一延迟。
仓库链接及感谢想看看整个项目吗?点击这里哦!点击这里查看!
此外,特别感谢Lior Yardeni(Lior Yardeni 链接),我的黑客松搭档,我们一起制作了这个游戏。
来总结一下吧在这篇文章中,我们学习了如何使用Python创建将语音转文字再转语音的游戏,并将其与AI结合在一起。我们使用了OpenAI
的Whisper
模型进行语音识别,使用FuzzyWuzzy
库匹配文本,并通过其开发者API与ChatGPT
进行对话,最后使用pyttsx3
将文本转换为语音。尽管OpenAI
的服务(如Whisper
和ChatGPT
的开发者版本)有费用,但总体来说还是很经济的。
希望这个指南对你有启发,并激励你开始着手你的项目。
加油!编程和乐趣!🚀
共同学习,写下你的评论
评论加载中...
作者其他优质文章