使用树莓派,用人工智能创造一个真正“神奇”的智能镜子
扫描二维码
随时随地手机看文章
在这个项目中,我将带你通过使用树莓派和双子座制作一个神奇的镜子。我特别使用Gemini Live API,它目前处于预览状态,所以如果你在发布后一段时间查看本教程,可能需要稍后更新,但没关系!玩得开心,我希望你能学到一些有用的东西。
这个项目也使用了新的JavaScript/TypeScript SDK,所以你可以在这里找到扩展项目的文档。
初始硬件设置
这个项目是基于这个魔镜项目。虽然我非常接近他们建造镜子的方式,但我确实做了一些不同的事情,我将在这里介绍。也就是说,由于显示器的尺寸、可用的材料和工具以及其他一些因素,你如何构建镜子很有可能会有所不同,所以这更多的是关于我如何构建镜子的解释,希望它是对原始(伟大)教程的有益补充。
这里是我在这个项目中使用的材料列表(我将在亚马逊上链接到它们,尽管取决于你什么时候读到这篇文章,这些链接可能不再有效,所以我将尽我最大的努力来描述这些项目)。我想说的是,我和我在这里买的/列出的任何东西都没有关系,它们只是碰巧是我挑选的或者已经让这个项目成功的东西——如果你有你认为会更好的东西,绝对要使用它,并在评论中告诉别人。
•树莓派。我使用了3B,因为我有大量的3B,我想如果我能让这个项目与只有1GB RAM的设备一起工作,那么如果其他人有更强大的主板,他们应该是黄金。确保你有一根适合Pi的电源线——我用的是一根我非常喜欢的带电源开关的电源线,这样我就可以很容易地打开和关闭东西。
•SD卡。我用的是32gb的存储卡,但有人可以使用更小的存储卡,也不会有问题。
•一个迷你HDMI到HDMI电缆。我使用一个使用左90度角,以更好地适应/隐藏它的设置。如果你使用的树莓派与3B的版本不同,或者是没有迷你HDMI接口的显示器,你可能需要一个不同的连接器。
•这款显示器(KYY便携式显示器15.6英寸)因为它又薄又小,最重要的是价格实惠。
•18x24x0.04英寸双向镜像丙烯酸。你要确保你买的任何东西都是双向镜面玻璃,因为它的设置方式是,显示器放在它后面,所以你想要有一个反射表面,让光线通过它。
•11.69x16.53x0.08英寸透明亚克力板。厚度并不重要,只是高度和宽度。这将被用作将树莓派和监视器内部连接在一起的一个部件。
•黑色的卡片纸。这是为了将镜子的边缘隐藏在显示器周围,所以你只需要确保它足够大,可以被切割成16x10.75”
•一个麦克风。我用的是AT2020,因为我已经拥有它了,但你应该可以使用任何你可用的。当我们进入本教程的代码时,我将指出需要更改的一个值以匹配麦克风的输入采样率(例如,我为AT2020使用44,000,但其他麦克风可能是16,000)。
•黑色管道胶带,用于粘贴东西和阻挡光线从卡纸的边缘进入显示器。
•双面安装胶带。这用于将监视器内部连接回它。
•M3螺钉和垫片。您将需要一些不同的长度,在原始教程中提到过。我个人选择了一个更大的品种盒,因为这些东西在很多不同的项目中都有使用。
•监控框拆卸工具。我买了一套便宜的电子开启工具,效果很好,但我只使用了几件。你还需要一个热风枪或吹风机来加热和软化将显示器电子设备粘在塑料框架上的胶水。
•可选材料的站立框架。我使用了一些我从另一个项目中得到的废弃中密度纤维板,并用激光切割了一个我在网上找到的免费支架(尽管我确实按比例放大了),但我认为你也可以使用画架或其他任何适合你的东西。我强烈建议在这个项目中使用某种支架。
好了,现在我想所有的材料都准备好了,让我们开始做这个很棒的项目吧。我将跳过如何拆卸显示器,因为我认为这在最初的教程中已经讲得很好了,我认为对于一个优秀的项目应该给予赞扬,所以去看看吧。
首先,我没有最稳定的手来得分和折断镜像丙烯酸,并以各种方式切割造成了很多碎片。经过几次失败的尝试(和破坏丙烯酸。对不起谷歌,但谢谢你让我花这个!),我把所有的床单切割成两个12“x18”的桌子锯,这工作得很好。如果你对其他方法更熟悉,那很好,但我只想说,这不是最容易处理的材料。
在我的镜像丙烯酸树脂变成一个可行的尺寸后,我想我可以省去下一个头痛,把所有的东西都移到激光切割机上。注意,如果你对激光切割机不太熟悉,我建议你在切割过程中,尤其是在切割卡纸的时候,把它放在宝宝身边。我第一次尝试的时候是不是把东西点着了?绝对的。以后我一不小心还会放火吗?有可能,但希望不是!一定要注意你的工具。
如果你碰巧使用我上面链接的显示器,我发现整体镜子的尺寸是16“乘10.75”,黑色卡纸上有一个13.25“乘7.25”的缺口。我还在M3螺丝的边缘添加了孔,这样你就不需要钻它们或3D打印夹具(尽管我确实在单独的尝试上打印了这些夹具,而且它们确实很好!)。我已经附上了一个PDF,这是96 DPI的文件,我设计的亲和设计师。你需要在每一块(镜像丙烯酸、透明丙烯酸和卡纸)上剪下绿色和红色的部分,但是中间的蓝色矩形只从卡纸上剪下来。
在这结束时,你应该有三个单独的材料全部削减到匹配的尺寸与对齐孔。在把所有东西拧在一起之前,别忘了把两个亚克力板上的塑料盖去掉!
对于此项目程序集的其余部分,请遵循原始教程。安装好树莓派后,你需要完成安装魔镜软件的步骤,确保显示器旋转90度,刻度正确,一切都在启动时启动。一旦有了基础项目,就可以深入研究新的Gemini连接魔镜代码了。
我强烈建议能够SSH到您的树莓派,因为本教程中的其他所有内容都将通过终端和git完成,尽管您也可以将键盘连接到镜像并直接与设备的终端交互。
初始代码设置
这个项目的所有代码都附在这个黑客身上。不过,如果你想直接在镜像上运行最新的代码,而不是从头开始构建它,你可以将这个github项目克隆到树莓派上Magic mirror项目下的modules文件夹中,运行npm install,然后更新你的配置文件以显示该模块。这将持续播放音频,所以你可能想要修改项目以考虑预算问题(例如,添加麦克风上的对话按钮),但由于这是一个黑客项目,你应该以任何对你有意义的方式进行修改。如果您处于嘈杂的环境中,请注意API非常敏感,并且在检测到新声音时容易中断。
您还需要更新配置文件以包含Gemini API密钥,该密钥可以在谷歌的AI Studio中创建或找到。
虽然作为该项目的核心的Live API每天可以免费提供有限数量的请求,但图像生成不是免费的(在撰写本文时),因此您可能需要根据您想要做的事情更改应用程序。您可以在这里找到定价的完整描述,因为新型号不断推出,具有不同的功能和定价。
这个项目的基础是魔镜模块模板,如果您想从头开始,可以在这里找到并克隆它。有了基本项目之后,就该更新魔镜的config/config.js文件了。作为参考,我对这个模块的添加看起来像这样:
最重要的部分是配置文件中的API密钥,因为这是驱动设置和可用信息的部分,并且它是本机的。
我们将在这个项目中使用三个主要文件:mm - gemini .js(尽管如果你克隆了模板,它将被称为mm - template .js),它处理所有的UI, mm - gemini .css,它为UI设置样式,以及node_helper.js,它是所有繁重工作发生的地方。为了简单起见,我将跳过css文件,但是您可以在这个项目页面或完成的GitHub项目中找到它的代码。
至于UI文件,我们基本上创建了一个UI状态机,它经历了initialize、READY、RECORDING和ERROR。socketNotificationReceived函数可以从helper接收有效负载和通知,帮助器告诉它应该在UI中显示哪个状态,然后它将更新DOM。
该函数还将接受GEMINI_IMAGE_GENERATING和GEMINI_IMAGE_GENERATED的通知,以便在调用该操作时显示进度旋转器或生成的图像(以base64格式接收),或者它将显示Gemini Live API生成的任何文本,直到收到turnComplete响应,然后在发送下一个响应时清除该文本。您可以找到这个项目附带的UI的所有代码,并将其作为起点。
如果一切工作如预期,你应该有一个类似于这样的UI:
将核心应用程序放在一起后,是时候深入研究node_helper.js文件,并真正使用Gemini了解这个魔镜的功能了!
设置Gemini Live API和语音输入
让魔镜工作的第一个主要步骤是,我们需要能够与魔镜对话,将音频数据实时发送到Gemini Live API,并等待响应。为了简单起见,我们将从请求以文本形式发送的响应开始,我们的UI将显示它。进入node_helper.js文件,让我们添加整个项目中需要的各种常量。
NodeHelper常量应该已经存在于您的基本项目中,因为Magic Mirror项目使用它来知道这是模块的帮助器。
由于这个项目使用的是最新的JavaScript/TypeScript SDK,我们需要导入谷歌/genai库,并包含多个将在整个项目中使用的类型对象。您可以从官方文档或源代码中找到有关这些类型的更多信息。
缓冲器和扬声器都与输出音频有关,我们将在本项目的后面进行。记录器是我们将用来记录从Pi的麦克风发送到live API的实时音频流。
进入下一个模块,INPUT_SAMPLE_RATE与您连接到树莓派上的麦克风有关。因为我使用的是AT2020,我的采样率是44100,但你的麦克风可能有不同的值。
OUTPUT_SAMPLE_RATE是我们开始播放接收到的音频时Gemini API的预期采样率。API目前仅输出24kHz。API使用一个通道,输出原始PCM音频数据,编码为带符号整数,16位。GEMINI_INPUT_MIME_TYPE是您将从Pi发送到API的数据类型。GEMINI_SESSION_HANDLE是一个值,我们稍后将使用它来保持关闭和重新打开之间的会话连续性,因为目前Gemini Live API将在大约十分钟后自动关闭。
最后,我们有GEMINI_MODEL。在撰写本文时,Gemini Live API处于预览阶段,因此该模型绝对会随着时间的推移而改变,这取决于您阅读本教程的时间。您需要检查Live API文档,以确保您使用的是项目的最佳选项。
有了这个集合,现在是时候添加将在整个项目中使用的所有变量了。我在NodeHelper.create()中初始化它们,然后我有一个applyDefaultState()函数,该函数可用于在会话关闭或发生错误时重置所有内容。我还添加了一组用于调试的日志功能。你不需要包括这些,但我发现它们在整个项目中很有用,所以我将把它们留在本文中。我还创建了一个名为sendToFrontend的辅助函数,用于封装将套接字通知发送到UI前端(mm - gemini .js)。
其中大部分用于维护状态,再加上一个closePersistentSpeaker()函数,我们将在音频输出时添加该函数。如果您正在跟随,请稍后再对其进行注释。您还会注意到genAI和imaGenAI的值。genAI将管理我们的实时会话,而imaGenAI是将用于图像生成的Gemini对象。如果你没有在项目中使用图像生成,你可以删除它。
现在让我们进入一些好东西。我有一个名为initialize的函数,它将用于启动这个项目所需的大部分内容。现在我将发布一个编辑后的版本,我们可以随着时间的推移添加。
这段代码验证API密钥是否可用,创建用于使用Gemini API的GoogleGenAI对象,然后创建一个新的LiveSession。这个实时会话使用SDK内置的web套接字框架来处理双子座模型和树莓派之间的发送和接收数据,它有一组回调,将驱动设备的状态。最重要的是onmessage,它将把双子座的响应发送到一个新函数,该函数将决定镜子应该如何反应。这里还有onclose的代码,它将重置回放状态和一些其他尚未编写的东西,所以可以随意注释掉onclose回调,直到最后。
您还会注意到一个配置对象。这将是这个项目的核心部分,因为它将包含与我们正在做的镜子和双子座API相关的每一个设置。现在它只有一个响应的模态。TEXT,这意味着我们希望Gemini模型只在onmessage回调中使用文本进行响应。
下面转到helper的socketNotificationReceived函数,我们想要进入通知开关语句并为START_CONNECTION和START_CONTINUOUS_RECORDING添加新的用例。START_CONNECTION将调用initialize,它将告诉前端在初始化完成后更新它的UI状态。一旦UI状态被更新,就会收到另一个开始录制的通知。完整的函数是这样的:
实际上录音是一个很大的步骤,所以我会把它分成更小的部分。我们将从创建一个名为startRecording()的新函数开始。这将检查设备是否已经在录制,连接是否已经打开,或者直播流是否正在运行。如果其中任何一个为真,则该函数将提前退出。
如果一切都没有问题,那么就可以开始录音了。我们可以更新isRecording状态值,然后创建一个recorderOptions对象。
从这里开始,是时候调用我们在文件顶部定义的记录器上的record,然后存储对音频流的引用。我还在这里创建了一个chunkCounter用于调试,但是如果您想要更简洁的代码,可以忽略它。
audioStream将为通过的任何数据提供侦听器。如果它不是空的(如果你拔掉麦克风,或者当你不使用麦克风时设置了一键接通),那么它会将音频数据转换为base64编码的字符串,然后使用liveSession将其作为新的JSON有效负载发送到Gemini Live API。sendRealtimeInput函数。
对于这个函数的其余部分,我有用于错误、结束和退出的侦听器,我将在这里包括它们以完成。
我还有一个stopRecording()函数,用于错误情况和直播流关闭时。重置所有内容和更新UI非常简单,所以我不会在本教程中深入讨论。
最后,让我们添加handleGeminiResponse()函数。该块将针对从Gemini Live API获得的各种类型的响应进行更新,但对于基本版本,我们只需要检索消息的内容并检查是否存在文本。如果是,我们会将文本块发送到UI显示。我也有一个if语句来检查安装是否完成,但我目前没有做任何与我的这个项目的版本。
此外,我们可以检查Live API是否说我们已经完成了响应回合。这是一个非常有价值的响应,因为它告诉我们模型何时完成生成文本,之后它会告诉我们何时应该完成音频响应的播放。你现在可以将这个块添加到handlegeminiResponse中,因为它将被UI用于清除响应之间的文本。
好了,这是很多,我略过了一些因为有很多状态管理的模板,但在这一点上你应该能够和镜子对话并显示来自Gemini Live API的文本响应。
音频响应和中断
既然我们有了文本,让我们深入研究如何获得音频,因为老实说,魔镜应该真的在和你说话。其工作方式是,我们将responseModality设置为modal。当Gemini Live API响应时,它将发送一个base64编码的字符串,用于多个可以在设备上播放的音频块。因为这些回应来得很快,而不是在他们大声播放的时候,我们还需要创建一个排队系统,当任何当前的音频播放完毕时,它就会转移到下一个音频块。最重要的是,Gemini Live API支持中断,所以如果用户在播放音频时说了什么,我们可以清除队列,让扬声器保持打开状态,等待下一个音频响应从API返回。
让我们从更新实时会话responseModality开始。
我们还将创建一个名为processQueue的新函数来处理回放逻辑。让我们一步一步地复习一下。首先,我们想看看队列中是否有任何东西。如果没有,那么我们可以关闭扬声器,假设队列没有被中断清除,并期待很快会有更多的音频块。
接下来,我们可以将processingQueue标志设置为true,用于状态管理。
然后我们要检查我们的扬声器是否已经创建,否则我们将创建一个新的。我想在这里指出的一件事是,我特别使用了一个存储在类级别的持久扬声器,因为我们想要最小化扬声器被创建和销毁的次数。这是一个很好的内存平衡,如果你使用的是内存超过1GB的新树莓派,这可能不是一个问题。
一旦我们知道我们有一个可用的扬声器,我们就可以从队列中检索base64编码的音频剪辑字符串,并在将缓冲区发送给扬声器播放之前将其写入缓冲区。
如果到目前为止一切顺利,我们将希望查看队列中是否还有剩余的内容,然后让processQueue函数调用本身移动到下一个块,否则我们将转义整个播放循环。
在这里,让我们定义用于错误情况的closePersistentSpeaker函数。这并没有做太多可怕的事情,除了关闭扬声器,移除听众,并试图清理我们的状态。
最后,在测试之前,让我们确保我们正在处理从Gemini Live API返回的音频和中断消息类型。我们可以通过向handleGeminiResponse函数中添加以下代码块来实现这一点。
现在,您应该能够重新启动镜像模块以与它进行对话,也可以在音频播放过程中中断它以更改对话的过程。很酷,对吧?
函数调用,搜索基础和图像生成
现在我们已经有了项目的核心,是时候更进一步了。函数调用是我最喜欢的Gemini API特性之一,因为它可以让任何使用Gemini的设备或应用基于与模型的交互来做一些非常有趣的事情。为了使用镜像启用函数调用,我们需要回到initialize中的配置对象,并添加一个tools数组。这将包括一个functionDeclarations数组,其中包含一个生成图像的函数(我将其命名为generate_image),以及Gemini模型用来知道何时应该调用该函数的描述,以及与该函数相关的任何其他指令。对于这个案例,我告诉镜报,它应该是异想天开和有趣的,同时使用幻想的绘画风格。我们会在镜子中添加更多的个性。在这个单独的函数中,我们还需要为将用于生成图像的提示符包含一个参数。
除了函数调用之外,我还在本节中向镜像添加了另外两个工具。第一个是谷歌搜索。这使得Gemini模型可以使用谷歌提供的各种工具,例如查找天气或当前时间。我还启用了googleSearchRetrieval,允许镜像进行谷歌搜索,在适用的情况下找到有关请求的最新和最相关的信息。您可以在functionDeclarations上面的tools数组中添加这两个工具。
此时,我们应该能够期望Gemini Live API触发函数调用,因此让我们确保在handleGeminiResponse中接受这些消息。返回到该函数,我们可以检查消息中是否存在函数调用块,然后我们可以将函数调用负载发送到将处理该代码的单独函数。
在handleFunctionCall中,我们将确保拥有函数所需的所有信息,然后使用switch语句确定调用了哪个函数。因为我们现在只支持一个函数,所以我们要么生成一个图像,要么退出这个函数。
由于这使用了一个独立的模型,而不是用于Live API的Gemini模型,因此我们还需要确保在initialize中初始化了适当的GoogleGenAI对象。
现在让我们试一试,让镜子在给我们讲故事的同时创造出一个物体的图像。
由于我们已经启用了搜索接地功能,我们可以询问当前的事情,比如我的家乡科罗拉多州博尔德的时间和天气。
添加个性
经过我们到目前为止所做的一切,我们终于有了一面可以工作的魔法镜子,但它感觉并不“神奇”,不是吗?让我们使用Gemini SDK中提供的一些工具来修复这个问题,这些工具可以赋予模型一点个性。回到我们的配置对象,让我们添加一个systemInstruction对象。我们会告诉人工智能,它是一个无所不知的、强大的魔法镜子,有趣、异想天开、轻松愉快,它从与人互动中获得快乐,用自己的知识和能力让人惊叹。
我们还可以添加一个新的speechConfig对象,它允许我们将声音配置为一些不同的东西。有一个视图的声音是可用的,所以你应该玩不同的,看看哪一个最适合你。以下是目前可用的简短列表,但未来可能会扩展:Puck, Charon, Kore, Fenrir, Aoede, Leda, Orus和Zephyr。
如果我们想要更多的定制声音,我们也可以给它一个语言代码。就我个人而言,我觉得魔镜应该说法语,所以我的语音配置是这样的,尽管我也很喜欢英语的“Puck”的声音。
结束
在这一点上,事情看起来很棒,所以让我们做更多的润饰项目,以真正使这个项目脱颖而出。我遇到的一个问题是,我需要不断地告诉AI完成它的故事,所以让我们在系统指令中添加一个新句子:“当你从一个故事中中断以显示故事中的图像时,请在调用函数后继续讲述故事,而不需要提示。你还应该尽可能在没有用户输入的情况下继续写故事——你是无所不知的镜子,用你对故事的了解让观众惊讶。”
在对话过程中,镜子也会尝试恢复为英语,所以让我们直接告诉它以用户使用的语言与镜子交谈,通过在系统指令中添加另一句话来响应用户:“如果检测到非英语语言,则以说话者输入的音频语言进行响应。”请用说话者通过音频输入的语言准确无误地回答。”因为这是我们可以开箱即用来支持多种语言的东西,所以我非常喜欢它。
这就是这个项目的全部内容!你还可以做很多事情来修改它,所以如果你建造了自己的魔镜,一定要玩得开心。添加一个摄像头,尝试生成视频显示给用户,玩语言和个性,或尝试创建代理系统,真正把镜子变成你自己的个人神奇助手。
本文编译自hackster.io