构建Ellee -一个GPT-3和计算机视觉驱动的会说话的机器人泰迪熊,具有人类级别的对话智能

我如何制作了一个可以移动头部并自然交谈的机器人泰迪熊。

Dexie正在和Ellee交谈(图片来自作者)

我有一个相当长的三周圣诞假期,尽管大部分时间都是和家人在一起,但在假期结束前一周我还有时间打发,所以我决定做一些有趣的东西!我一直想为GPT-3人工智能模型找到一个好的用例,我也想为我们三岁的儿子Dexie做一些有趣、互动和有教育意义的东西,他非常健谈和好奇。所以,我想到了做一个会说话会动的机器人泰迪熊。

概念

这个ro机器人泰迪熊将能够看到并认出一个人,叫出他们的名字,并与他们交谈。她将有听力能力去听别人说什么,并产生自然的声音回应。她需要能够理解对话的上下文,并相应地做出回应。稍后我将对此进行更详细的阐述。在整个对话过程中,她需要能够移动她的头来看着对方的脸。

为了最大限度地提高结合系数,我决定把德克西最喜欢的玩偶艾拉变成这个机器人。我给了她一个新名字,埃莉,代表电子版本的埃拉。

他最喜欢的泰迪熊玩偶Dexie和Ella(图片来自作者)

研究

为了实现上述目标,Ellee需要具备以下几个模块:

  • 视线.Ellee必须能够跟踪一个人的位置,并实时识别他们的脸,这样她就可以移动头部看着他们,并叫出那个人的名字。为此,我需要有一个连接到人工智能系统的摄像头,以检测一个人的存在和位置以及他们的脸,并识别他们。需要一个经过训练可以识别人体和面部的物体检测AI模型,该模型将在连接到摄像头的gpu供电设备上运行。
  • 头部运动。Ellee需要能够左右和上下转动她的头(两个自由度)。
  • 听力。Ellee需要能够听到对话,这需要语音识别技术和麦克风。
  • 大脑。Ellee需要能够理解谈话内容,并通过考虑过去的对话来提供一些上下文来生成自然的文本回应。这需要一个强大的生成文本AI模型。
  • 演讲。Ellee需要向这个人打招呼,并说出由大脑模块生成的文本回应,这需要文本到语音的技术。
  • 协调员。这是连接所有组件所必需的。

根据我过去的经验项目经过一番研究,我列出了运行这个系统所需的硬件清单。

  • NVIDIA Jetson Nano(150澳元)。这是一个小型gpu驱动的嵌入式设备,可以运行所有模块(特别是物体检测和面部识别AI模型)。这是一个完美的设备,因为它可以通过USB端口支持麦克风和音频输出,并且它有一个以太网端口,可以方便地上网进行API调用。你甚至可以插入鼠标和键盘在设备上进行开发和调试,因为它有一个功能齐全的Ubuntu 18.04操作系统。
NVIDIA Jetson Nano
  • USB麦克风和扬声器(AUD 20美元)。这些听起来可能并不复杂;然而,有人报告说一些扬声器和麦克风不能在Jetson Nano上工作,所以希望你能发现这些细节有用。我可能很幸运,因为我买的唯一一个USB扬声器在稍微摆弄了一下嘶嘶的反馈声音后还能用。但是,我买的两个麦克风中只有一个(USB那个)可以用。品牌和型号请看下面的照片。
扬声器和麦克风(图片来自作者)
  • 相机-索尼IMX219 160(AUD 35美元)。这是一个很棒的微型160度POV 800万像素覆盆子相机,使Ellee能够看到和识别人。根据我在其他机器人项目中的经验,广角POV是至关重要的——否则,Ellee将无法发现任何人,除非他们正对着她,这感觉不自然。
  • 伺服电机(75澳元)。两个5公斤/厘米扭矩伺服电机连接到平移和倾斜支架将允许两个自由度旋转。需要PWM驱动器来驱动电机,因为Jetson Nano GPIO引脚只能提供1 mA的电流,而伺服电机则需要3 A的电流。由于电机只需要移动Ellee的头部,这是非常轻的,5公斤/厘米的扭矩绰绰有余。
伺服电机,支架和PWM驱动器(图片来自作者)
  • 木棍和一个柜子(AUD 10美元)。这根木棍将作为Ellee的骨架结构来连接相机和伺服器。这根棍子将连接到隐藏硬件组件的木柜子上,Ellee将坐在上面。
Ellee的骨骼结构和柜子(图片来自作者)

实现

有了一个坚实的计划,我开始完成我的使命。

构建视野

为了从摄像机获取视频,我使用了一个名为Jetcam的库,它是我扩展的。在4行代码中,你可以让整个程序运行起来:

从jetcam。csi_camera import CSICamera
camera = CSICamera(width=224, height=224, capture_width=224, capture_height=224, capture_fps=30)
Image = camera.read()

接下来,需要一个物体检测组件来分析视频帧,并检测人体和面部的位置,以便能够跟踪和查看它们。请记住,NVIDIA Jetson Nano的GPU远不如桌面级GPU卡(如RTX)强大,因此选择在精度和性能之间取得良好平衡的对象检测模型架构至关重要。

在过去,我一直使用MobileNetSSDV2模型架构来构建在Tensorflow上运行的对象检测模型,它在精度和性能(10FPS)之间提供了很好的权衡。这一次,我使用的是MobileNetSSDV2模型NVIDIA JetPack SDK运行在PyTorch上,非常简单,只需添加三行代码:

进口jetson.inference
net = jetson.inference.detectNet("ssd-mobilenet-v2", threshold=0.1)检测(图像,宽度,高度)

令我惊讶的是,该模型提供了20FPS。甜蜜的!我不需要建立我自己的模型,因为它已经支持91个COCO类,包括我需要的人体。

我使用了Dlib(一种现代机器学习框架)来检测和识别人脸。我可以建立自己的自定义对象检测,包括人脸作为类之一;因此,我不需要运行另一个人脸检测,这可能会增加一些性能。然而,我决定不这么做,因为我一直想尝试一下Dlib库,这是一个完美的时机。此外,我懒得建立自己的对象检测模型,而不是简单地添加上面的三行代码。
用Dlib检测人脸非常简单——两行代码给出了检测到的人脸包围框及其置信度分数的列表。

进口face_recognition
face_recognition.face_locations(图片)

为了识别人脸,Dlib提供了两个重要的功能。第一个函数是face_encoding,它从人脸图像中计算出指纹,称为编码。这种编码唯一地识别了一张脸。第二个函数是face_distance,它计算人脸编码与人脸编码列表之间的距离(不相似度)。结果是一个距离列表,每个面一个。距离最小的脸就是最相似的脸。

这就是我使用它们的方法。首先,使用face_encoding函数,我在应用程序开始时生成了所有家庭成员的面部编码,并将它们保存在一个列表中。在运行时,我为视频帧中检测到的每个人脸调用face_encoding,并将结果传递给face_distance,以计算传递该编码与包含我的家庭成员编码的列表之间的距离。最后,我对结果进行升序排序,并选择了第一个(假设他们都不是我的家庭成员,它也通过了最小距离阈值)。这是不是意味着Ellee再也认不出其他人了?我添加了一个功能,让她能够自动注册一个新的未识别的人的脸,我将在后面的部分介绍。

物体检测与人脸识别(图片来源:作者)

检测/识别精度非常好。然而,在Jetson Nano中,face_encoding和face_locations函数都需要大约0.5秒的时间来执行。因此,为每个视频帧调用它们将显著降低系统FPS从20FPS到1FPS。这是不可接受的,因为这将在Ellee认为的人脸位置与实际位置之间引入+1秒的滞后,这将导致她的头部跟随某人的人脸具有+1秒的滞后。

解决方案是在线程中以较低的FPS调用面部检测/识别,例如每10帧调用一次。考虑到摄像头的拍摄速度设置为10FPS,这意味着我们每秒都在进行面部检测/识别,这还不算太糟。我使用相同的技术每两帧运行一次目标检测,这将最终性能提升到12帧/秒。

降低物体检测和人脸检测/识别帧数(图片来源:作者)

虽然这个技巧有效,但我注意到它给Ellee的头部带来了+1秒的延迟,即它跟随人的面部有一秒的延迟,因为检测到的面部位置每秒才更新一次。我通过不使用检测到的面部位置来进行头部跟踪来解决这个问题。相反,我伪生成了人脸的位置,假设在垂直方向上,它距离人边界框的顶部有5%的距离,在水平方向上,它位于人边界框的中心。这个假设很有效,因为大多数时候,被检测到的人的头部总是在视野中。

人脸包围框的伪生成(图片来源:作者)

建立头部运动

头部运动模块控制的角度,每两个伺服通过adafruit_servokit框架达到目标的航向和俯仰。Adafruit servokit是一个兼容树莓派的框架,允许你用几行代码来控制伺服电机。由于我们使用PWM驱动器来控制伺服电机,我们需要将控制信号从连接到PWM驱动器的Jetson Nano SCL和SDA GPIO引脚发送出去。因此,在初始化时,我们配置伺服套件与SCL和SDA引脚。我们只需要驱动两个伺服;然而,这个PWM驱动器可以驱动多达16个伺服电机。要实际移动伺服电机,我们只需将一个值赋给self.kit。伺服[servo_no]通过传递适当的servo_no。

from adafruit_servokit导入ServoKit
进口板
进口busio
#初始化
self.kit.servo [servo_no]。角度=值
i2c_bus0 = (busio.I2C(board. busc)。SCL_1, board.SDA_1))
自我。kit = ServoKit(channels=16, i2c=i2c_bus0 .
#移动伺服[servo_no]到一个特定的值
self.kit.servo [servo_no]。角度=值

请注意,伺服值并不直接对应于实际角度。因此,我们需要在代码中执行一些归一化来将角度值映射到伺服值。

非连续伺服电机(我们的)只允许最大180度旋转。我们不需要Ellee的头旋转超过160度,无论是朝向还是俯仰。因此,我们进一步在控制代码中实现了最小和最大范围限制,将航向从10度限制到170度,俯仰从35度限制到90度。

头部旋转轴和极限。(Bear Image授权自shutterstock)

所以,为了让Ellee移动她的头部面向被检测的人,我们所需要做的就是将被检测的人的面部的x坐标转换成一个相对于Ellee当前头部方向的方向角,并将对应的伺服与映射值进行相应的设置。然后我们重复这个过程。
虽然这看起来很简单,但直接将伺服值设置为目标角度有一些缺点:

  • 伺服运动非常快。一旦你设定了一个值,伺服系统就会很快地以恒定的速度向这个值移动,这让人感觉非常机械。人类的头部移动速度变慢,然后逐渐减速,直到完全停止在最后的位置。
  • 两个伺服器以相同的速度移动。事实上,我们无法控制速度。如果航向和俯仰伺服需要从其当前值移动不同的量,需要移动较少的将首先到达目的地。下图左边的图中说明了这一点,其中绿色圆圈表示初始值,蓝色圆圈表示最终值。纵轴表示音高,横轴表示标题。蓝线代表头部运动。你可以看到,头部和俯仰最初都以相同的速度移动,直到俯仰达到其最终值10。然后,只有标题移动到最终值90。这是不自然的,因为当我们移动头部时,头部和俯仰都会同时到达目标。

很明显,我们需要能够以可控的速度将两个伺服系统独立地移动到新的目标值,这样我们就可以使它们缓慢移动并同时达到目标值。

用插值控制航向和俯仰(图片来自作者)

同样的问题也存在于游戏产业中,比如虚拟角色的头部动画。解决方案是使用插值技术将移动路径划分为几个路径点,并逐步递增地将航向和俯仰都移动到最终位置,如上图右图所示。橙色点表示每个路径点,以设置从起始值到最终值的伺服值。最初,步长很大,当接近最终位置时,步长逐渐变小。这将使头部逐渐减速直至完全停止。

建立听力

听觉模块负责通过麦克风收听语音,并使用语音识别技术将其转换为文本。延迟在这里非常关键,因为处理时间越长,Ellee在对话中响应所需的时间就越长。理想情况下,您希望在边缘(设备中)运行语音识别以避免互联网延迟。然而,在边缘上运行需要一个强大的设备,据我所知,在撰写本文时,没有一种边缘语音识别技术可以与Jetson Nano的计算能力相媲美,接近谷歌语音识别,这是我可以接受的标准。这就是为什么iPhone的Siri、谷歌Home和亚马逊的Alexa都将我们的声音发送到云端进行语音识别。

因此,我决定使用谷歌语音识别云服务。为了最大限度地减少延迟,我使用了流式技术,即连续地将检测到的语音块发送到云端,以便在人说完整个句子之前执行识别。使用这个技巧,我成功地在人们说完一句话的1.5秒内获得了识别的文本结果,而不管句子的长度。

谷歌语音识别(官方标识图)

构建大脑

Ellee的大脑负责从当前对话中生成文本回应。因此,我们需要聊天机器人技术。听起来很简单,但实际上这是Ellee最复杂的部分。为什么?首先,它需要理解这个人说的最后一句话,以产生适当的回应。这本身就已经很复杂了。这就解释了为什么人类儿童需要三年时间才能掌握基本的对话技能,而掌握这些技能则需要很多年。但这还不是全部。为了产生一个适当的回应,你还需要理解对话的上下文,它来自过去所有的对话。看看下面的对话。 For Ellee to correctly answer the question ‘Which city was it?’, she first needs to look at the past few exchanges to understand that we were talking about Albert Einstein and the time of his death. Without that context, this question could be interpreted to mean the city in which he was born or lived. Not only does Ellee need to master linguistics, but she also has to acquire historical knowledge to be able to answer this question.

你知道阿尔伯特·爱因斯坦吗?

:上诉人与被上诉人签订是的,我知道爱因斯坦。

人类:他什么时候死的?

:上诉人与被上诉人签订他于1955年去世。

人类:是哪个城市?

:上诉人与被上诉人签订那是新泽西州的普林斯顿

然而,人们可能会问一个需要其他知识领域的问题,如电影、音乐、数学、化学、体育等。艾莉需要掌握所有这些领域。如果很难,他们是如何构建谷歌Home、Siri和Alexa的?它们都是基于检索的聊天机器人,只能回答已经准备好并存储在其庞大数据库中的问题,因此有了“检索”这个术语。试着问上面的问题,你会看到这些聊天机器人是如何悲惨地失败的。

为了实现上述要求,我们需要一个基于生成的聊天机器人,它可以根据直觉逐字生成响应,即通过理解所说的内容和对话的上下文。

我在过去使用各种技术构建了几个聊天机器人,从检索到生成,没有一个接近满足上述要求。

见到GPT-3 !这是通用NLP AI模型的最新突破之一,由OpenAI团队构建,并使用来自维基百科和书籍的45TB文本进行训练。事实上,维基百科只占其训练集的3%,所以你可以想象这个模型的庞大规模。它的训练成本高达1200万美元,有1750亿个参数!

OpenAI(官方logo图片)

GPT-3的独特之处在于,它是一种通用语言模型,可以通过简单的人类语言给出指令来完成任何与文本相关的任务。这使得GPT-3可以执行各种任务,如完成一首诗、写一份商业计划书、执行情感分析和文本分类,而不需要提供通用NLP模型所需的数百万个训练集。

为了在GPT-3中构建Ellee,我只需要用简单的语言用这条指令训练它。

以下是与Gus创造的AI Ellee的对话。Ellee住在澳大利亚的Mitcham。爱莉喜欢和人交谈。她最喜欢的颜色是绿色。Ellee乐于助人,富有创造力,聪明,而且非常友好。

AI:嗨(名字)!你今天想聊什么?

人类(human_response):

第一段是至关重要的,它给了Ellee一个影响她如何交谈的个性。我希望她是有创造力和友好的。向她提供一些背景信息,比如她出生在哪里,谁创造了她,她最喜欢的颜色,这样她就能在回答中使用这些信息。例如,回答以下问题:你住在哪个国家?你喜欢绿色吗?你叫什么名字?

我把面部识别模块识别的人的名字放在[name]下面,把语音识别模块识别的文本响应放在[human_response]下面。这是所有。GPT-3将生成下一个Ellee的响应,我将其与下一个被识别的人类响应一起添加回指令中。

为了帮助我们解释,我们假设她正在和德克西说话,德克西回答说“我很好,谢谢。”让我们来谈谈体育。Ellee对此的回答是“我真的很喜欢运动。你最喜欢的运动是什么?德克西回答说:“我爱篮球。”下一个培训指令将变成以下内容:

以下是与Gus创造的AI Ellee的对话。Ellee住在澳大利亚的Mitcham。爱莉喜欢和人交谈。她最喜欢的颜色是绿色。Ellee乐于助人,富有创造力,聪明,而且非常友好。

嗨,德克西!你今天想聊什么?

人:我很好,谢谢。让我们来谈谈体育。

AI:我真的很喜欢运动。你最喜欢的运动是什么?

人类:我喜欢篮球

GPT-3将生成Ellee的下一个响应。这个过程会重复发生,直到对话结束。通过这种方式,GPT-3将拥有过去对话的背景,从而能够产生更好的响应。为了尽量减少处理时间和成本,我将过去的对话限制在最多20次。GPT-3需要强大的计算能力才能运行;因此,它可以通过对OpenAI web服务的API调用来访问。

现在,大脑完成了。事实上,之前关于爱因斯坦的对话交流是和Ellee的真实对话,她可以正确回答最后一个问题!

你可以在本博客末尾的视频最后部分亲眼见证Ellee的对话能力。

建筑物名称提取

除了产生文本回应外,大脑模块还负责识别与Ellee对话的人的名字。这个超出范围的要求是出乎意料的。我认为这将是非常酷的,当Ellee不认识她说话的人,她可以提取他们的名字,如果在他们的谈话中被提到,并注册他们的面部图像。因此,在以后的谈话中,她会知道他们。

我们已经从观察模块中获得了他们的面部图像我们也知道艾莉认不出他们。但是,Ellee如何能够从下面的示例对话中提取他们的名字呢?

AI:你好。我叫Ellee。你今天好吗?

人类:早上好,Ellee。我今天很好。你好吗,Ellee?

AI:我也很好。你在忙什么?

人类:我只是和我妻子莫妮卡在下午散步。很高兴认识你。我是约翰。

你想和我聊天吗?

人类:我很乐意。你会怎么做?

解决这个问题的常用方法是构建一个命名实体提取NLP模型。人工智能模型训练了数十万个带有标签的句子,其中的名字是学习模式,找出哪些单词是名字,并使用这个模型来识别对话脚本中的哪些单词是名字。

一个名称实体抽取训练集的例子

这是一个费力的过程,考虑到我们只对与Ellee谈话的人的名字感兴趣,而不是任何名字,这使得它更具挑战性。我们需要某种转换器模型,它能够理解围绕已识别名称的上下文,以捕获我们感兴趣的名称。

嘿,这难道不是GPT-3作为通用语言模型应该能够解决的任务吗?哦,天哪,确实如此!我是这么做的。我首先剔除了Ellee的所有对话交流,只留下与人类对应的交流。然后我用下面的简单指令构造了一个训练指令。

以下是一个名为Ellee的人工智能和一个人之间的对话。从对话中提取这个人的名字:如果没有找到名字,我将回复“未知”。

早上好,Ellee。我今天很好。你好吗,Ellee?

我只是和我妻子莫妮卡下午散步。很高兴认识你。我是约翰。

我很乐意。你会怎么做?

名称:

这是我唯一要告诉GPT-3的事。它神奇地附加了名字约翰就在提供的旁边名称:

即使有人提到了莫妮卡,它也知道莫妮卡不是我们谈话对象的名字。

这是非常令人兴奋的,完全改变了游戏规则。

构建演讲

我使用亚马逊波利用大脑生成的文本合成艾莉的声音。这是另一个增加了200ms延迟的云服务。然而,声音的质量是惊人的自然。

亚马逊Polly(官方标志图片)

协调员

协调器工作是通过跨模块发送数据来将所有模块粘合在一起。它有一个状态机来跟踪Ellee的当前思维状态,这决定了她接下来要做什么,例如开始听,停止听,开始说话,移动她的头,重置她的头的位置,等等。例如,当Ellee第一次看到Dexie时,控制器以Dexie为焦点人员创建了一个新的会话。这一点至关重要,因为有时可以检测到不止一个人,我希望Ellee能够看到她一直在说话的同一个人。然后,控制器将从视线模块获取Dexie的边界框位置,计算并将新的航向和俯仰角发送给头部运动模块作为新目标,以便她的头部开始跟随他。当Dexie的可见时间超过两秒时,控制器将指示语音模块向他打招呼并开始收听。当一个句子讲完后,它会从听力模块中抓取已识别的文本,并将其传递给大脑,通过API调用GPT-3生成响应并等待响应。在得到响应后,它将抓取响应文本并将其传递给语音模块以进行说话。当Dexie不再可见超过10秒时,控制器将重置对话会话,并准备寻找下一个可见的人。

组装

完成所有模块后,现在是组装硬件的时候了。在我父亲木工技术的帮助下,我们建造了艾丽的骨架结构。这是一根棍子,我可以把两个伺服(航向和俯仰)和相机安装在俯仰伺服。

我父亲的工作室和木制道具(图片来源:作者)

然后把这根棍子放进Ellee的身体里,装在她坐的木箱上。这两个伺服器将在她的头部内,颈部关节的位置,并确保Ellee的头部与一个螺钉,以确保她的头部与伺服器一起移动。添加了几层木材,以扩大相机的范围,这样它就在正确的位置,从她被移除的左眼球中出来。

(图片来自作者)

我花了几次迭代才做到这一点,这实际上是我在这个项目中花费大部分时间的地方。最后,我们完成了组装!然而,有一些主要的问题。埃莉的脖子看起来就像刚刚被弗里迪·克鲁格(Freedy Krueger)割开了一样,尤其是当她抬起头的时候。她的左眼球,也就是相机,比右眼球高,由于相机拉起了原来的洞,导致它撕裂和扩大,所以有一个很大的可见洞。这个长得像科学怪人的艾莉和德克西相处不好。我需要做点什么。

(图片来自作者)

幸运的是,凭借我妻子神奇的缝纫技术,以及去一家面料和工艺品供应商Spotlight买了几条白毛巾和缝纫工具,我们修好了她。

(图片来自作者)

如下图所示,她现在看起来好多了。我们给她做了一条围巾盖住她喉咙上的洞,这也让她更时尚了。:)

(图片来自作者)

最后,我们让艾丽坐在爸爸做的一个小木柜子上,把棍子固定好。这个机柜有足够的空间容纳Jetson Nano, PWM驱动器和所有的布线。

(图片来自作者)

Showtime

最后,是时候让Dexie第一次和Ellee互动了。他不知道我要把艾莉变成机器人的计划,所以看到他对艾莉的第一反应真是无价!他在埃莉周围转来转去,想弄明白她究竟是怎么行动和说话的。当他慢慢适应时,我们和Ellee聊得很愉快。任务完成耶! !

包括建造过程在内的整个体验都记录在下面的视频中。一定要看最后一部分,我用一个更复杂的对话测试了Ellee。

Ellee演示视频

我对系统的性能很满意。它以12FPS运行,尽管运行了这么多模块和几个AI模型。她的头部运动也非常敏感,跟随她正在说话的人。对话延迟,也就是Ellee回应对话的延迟,最多只有2.5秒,考虑到语音识别、文本生成和文本到语音之间发生的事情,这还不算太糟糕,所有这些都要往返于互联网上。

未来的改进

制作Ellee非常有趣,也教会了我一些东西:

  • 我需要买一台3D打印机。制造一些道具组件要容易得多,因为你可以精确地设计并在需要时进行微小的调整,而不是手动砍木头。
  • 1 FPS的面部识别/检测并不理想,有时当检测到多人但面部识别没有起作用时,Ellee不可能知道她在和谁说话。为了提高面部识别/检测频率,我可能需要使用更强大的硬件,如Jetson Xavier NX或Jetson AGX Xavier。
  • 有时,当发现有很多人,而其中只有一个人在和Ellee说话时,她会看着那个没有说话的人,因为她会优先考虑那个离她更近的人和她认识的人。一个解决方案是添加一个意图检测AI模型来检测当前说话的人是谁。更棒的是,如果Ellee可以唇读,并与她的听力相匹配,即使两个人都在说话,她也可以确定谁在和她说话,如果他们同时说话,她可能会分割和分离他们的声音。这进一步证明了使用更强大的硬件是正确的。

隐私和道德风险

在构建涉及隐私和道德的人工智能技术时,我们需要负责任。首先,面部识别技术目前还不太流行,尤其是在隐私方面。Ellee的面部识别仅用于个性化她与你的对话,而不是用于跟踪或监视目的,因此我对此很满意。其次,从道德的角度来看,使用GPT-3构建的聊天机器人在其行为和事实方面可能很难控制。有时Ellee会编造一个完全合理但错误的事实,这可能会冒犯一些人。因此,请不要把她的回答太当真。应该避免将这种类型的聊天机器人用于医疗或情感咨询等具有严重后果的目的。

就是这样,伙计们!我希望你喜欢阅读我令人兴奋的暑假项目,就像我喜欢与你分享它一样。

完整的源代码是可用的在这里

如果你喜欢这个博客,请给我一些掌声,并在我的linkedin

Baidu
map