自定义交互设计和使用
1 实现流程
1.1 整体效果
VAD算法能实时判断周围的能量,并根据能量的门限判断是否识别为语音,如果识别为语音,则将其保存下来并通过离线关键词检测,判断语音中是否有唤醒关键词。若有有唤醒关键词,则反馈给唤醒状态机,否则机器人将保持休眠状态。
当唤醒状态机处于唤醒状态时,这时候下达的指令如果是有效的指令,则机器人会执行相应的指令并让唤醒状态机返回休眠模式。
以上是我们上一篇教程中所描述的语音交互整体流程。
在本章节中,我们将讲解用户如何自定义一个属于自己的唤醒词,并实现自己的语音交互动作的功能开发。
1.2 语音交互的基础,命令词的设置
首先我们需要修改科大讯飞的语法文件handsfree_speech/cfg/msc/res/asr/talking.bnf
#BNF+IAT 1.0 UTF-8;
!grammar control;
!slot <move>;
!slot <time>;
!slot <wake>;
!start <callstart>;
<callstart>:<control>;
<control>:<wake>|<move>;
<wake>:小道!id(99999)|小道小道!id(99999);//(此处可以修改唤醒词)
<move>:往前走!id(10001)|前进!id(10001)|向前进!id(10001)|后退!id(10002)|向后退!id(10002)|退后!id(10002)|向左转!id(10003)|左转!id(10003)|右转!id(10004)|向右转!id(10004)|退出语音模式!id(10005)|关机!id(10005);//(此处可以修改命令词)
我们的语音交互功能的实现方式是利用VAD算法来实时检测周围的声音环境。
在这里我们启动了一个线程,这个线程可以利用vad算法实时检测声音,并根据检测情况,判断声音是否达到阈值。当达到一定的阈值时,我们就会将其记录并保存这一段触发阈值的声音为一个wav文件。
通过修改语法文件,我们可以利用科大讯飞的离线语音命令词识别
功能来对我们前面保存的wav文件进行命令词的识别。这样就可以实现了实时语音命令词识别的功能。
故我们在语法文件中修改对应的唤醒词和命令词后,就可以通过不同的语音命令去唤醒或下达命令,从而实现自定义的语音唤醒和语音控制。
上面代码所展示的是我们默认的语音功能,下面有我们将讲解用户如何自定义添加一个功能。
1.2.1 下面是我们的数据处理的方法
static int16_t get_order(char *_xml_result){
if(_xml_result == NULL){
printf("no");
}
//get confidence
char *str_con_first1 = strstr(_xml_result,"<confidence>");
//printf("\n%s",str_con_first1);
char *str_con_second1 = strstr(str_con_first1,"</confidence");
//printf("\n%s",str_con_second1);
char *str_con_first2 = strstr(str_con_second1,"<confidence>");
//printf("\n%s",str_con_first1);
char *str_con_second2 = strstr(str_con_first2,"</confidence");
//printf("\n%s",str_con_second1);
char str_confidence2[10] = {};
strncpy(str_confidence2, str_con_first2+12, str_con_second2 - str_con_first2-12);
//printf("\n%s\n",str_confidence2);
char *str_con_first = strstr(_xml_result,"<object>");
char *str_con_second = strstr(str_con_first,"</object");
char str_confidence[10] = {};
char str_order[5] = {};
strncpy(str_confidence, str_con_first+25, str_con_second - str_con_first-45);
memcpy(str_order,str_confidence,5);
//printf("\n%s\n",str_order);
std_msgs::String msg_pub;
if(atoi(str_confidence2) <40)
{
msg_pub.data="00001";
pub.publish(msg_pub);
}
else
{
msg_pub.data =str_order;
pub.publish(msg_pub);
}
通过运行科大讯飞提供的demo,我们可以看到引擎反馈回来的数据如下例子所示:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?><nlp>
<version>1.1</version>
<rawtext>打电话给丁伟</rawtext>
<confidence>94</confidence>
<engine>local</engine>
<result>
<focus>dialpre|contact</focus>
<confidence>81|100</confidence>
<object>
<dialpre id="10001">打电话给</dialpre>
<contact id="65535">丁伟</contact>
</object>
</result>
</nlp>
这组数据包含了反馈回来的结果,我们主要关注的是其中相应的置信度参数设定的ID号以及相应的内容。
这里我们通过查找相应的字符元素来提取所需信息,从而获取相应数据的位置信息。然后将所需数据的信息提取出来存在一个数组里,再将其转换成string_msg
类型的ROS话题数据,并利用ROS话题去调度相应的反馈。
您也可以将上面的注释给取消掉,通过看注释来理解一下这个提取的方法。
在这里,我们主要提取出来的数据为ID号和置信度的数据。我们可以通过ID号来判断当前的命令是什么,通过置信度检查当前的命令有多少可能性是我们提出的命令。这里还可以进行一个命令置信度的一个筛查,将低置信度的命令给忽略掉。我们这里设定的阈值是40,这个值可以根据需要自行更改。当阈值过低的话,很容易触发错误指令。
由于机器人使用的是VAD算法来实时检测声音,如果周围环境有杂音的话,机器人也会实时检测到。故在代码中我们通过设置不同的标志位,来表示不同的状态,并利用标志位来实现一个简单的状态机。最后再通过状态机来控制机器人判断目前是否满足执行相应动作的条件。
此处的demo是将功能和唤醒分为两个模块来实现我们的控制需求,我们也可以基于这种模式来进行结构更加多样化的控制。当然这样需要的设计逻辑和需要考虑的方面也更复杂,这里我们只提供了一个简单的demo演示。
1.2.2 首先我们来看一下我们的语音唤醒的设计。
int ret;
time( &rawtime );
info = localtime( &rawtime );
int todo_id = atoi(msg->data.c_str());
if(todo_id==99999)
{
char* text;
int x =random(0, 5);
if(x==1)
{
text =(char*)"有什么事情"; //合成文本
}
else if(x==2)
{
text =(char*)"怎么啦"; //合成文本
}
else if(x==3)
{
text =(char*)" 来了"; //合成文本
}
else
{
text =(char*)" 我在"; //合成文本
}
printf("收到唤醒指令");
ret =text_to_speech(text, filename_move, session_begin_params);
if (MSP_SUCCESS != ret)
{
printf("text_to_speech failed, error code: %d.\n", ret);
}
printf("合成完毕\n");
ifwake=1;
std_msgs::String msg_pub;
msg_pub.data ="stop";
pub.publish(msg_pub);
play_wav((char*)"/home/handsfree/catkin_ws/src/handsfree_voice/res/tts_sample_move.wav");
msg_pub.data ="tiago";
pub.publish(msg_pub);
}
这里我们设计了一个随机函数,该函数是一个伪随机,即当随机种子固定后,出现的结果也是固定的。虽然是伪随机,但是该设计能够提升机器人和用户的一个交互性,让机器人的反馈语句不再是固定且死板的,而是可以在几个反馈语句中随机选择其一并进行反馈。
1.2.3 接下来就是讲解功能的添加
假设我们加一个时间交互功能。
则需要在<control>:<wake>|<move>;
处添加一个|<time>
;
即变成:
<control>:<wake>|<move>|<time>;
添加一个 <time>
:时间!id(10006)|现在几点!id(10006)|现在!id(10006);
添加完后,如果用户和机器人进行对话并使用了时间
的命令词,则程序会反馈对应的id(10006);
我们可以修改语音合成文件 tts_offline_sample.cpp
中的函数,来让机器人接收到的对应id命令时可以进行不同的行为处理。
比如:
if(todo_id==10006&&ifwake==1)
{
strftime(buffer,80,"现在时间是:%Y年%m月%e日,%H点%M分%S秒", info);//以年月日_时分秒的形式表示当前时间
const char* text =buffer; //合成文本
ret =text_to_speech(text, filename, session_begin_params);
if (MSP_SUCCESS != ret)
{
printf("text_to_speech failed, error code: %d.\n", ret);
}
printf("收到获取时间指令");
printf("合成完毕\n");
play_wav((char*)"/home/handsfree/talking/talking/catkin_ws/src/offlinelistener/config/tts_sample.wav");
ifwake=0;
std_msgs::String msg_pub;
msg_pub.data ="stop";
pub.publish(msg_pub);
}
if(todo_id==10006&&ifwake==1)
此处代码判断的是在接受到命令的同时机器人是否处于唤醒状态。如果同时满足这两个条件则执行后续的命令。
比如上述代码就是让机器人在满足条件后,读取当前计算机设备的时间信息,然后通过科大讯飞的语音合成功能将这个时间信息按我们设定的语句转换为wav文件,并将语音通过外放播报出来,从而实现人机语音交互。
1.3 这里的关机(预设ID:10005)命令也非常重要!!
由于我们的vad算法是通过启动线程并利用循环函数去控制,而ROS本身也相当于启动了一个线程,所以我们如果不进行统一处理的话,会导致进程卡死。故为此我们设置了关机指令
,利用关机指令可以结束我们其他的线程。包括后续要加入的语音巡逻功能也是如此,只要添加了线程类或者在一个循环里的ROS代码,都可以通过这条指令根据相应的关机话题去处理。
比如:
def roscallback(data):
if int(data.data)==10005:
global start
start= 0
print start
rospy.loginfo(data.data)
//这里就是设定一个全局的变量,跳出循环函数,这里我们的vad结束后语音就没办法接受数据了,所以我们这里一跳出
循环,程序就结束了,或者将这个线程杀死了。
pub = rospy.Publisher('vad_listener_hear', String, queue_size=10)
rospy.init_node('vad_listener', anonymous=True)
sub = rospy.Subscriber("order_todo", String, roscallback)
rate = rospy.Rate(10) # 10hz
global start
while start==1:
#下面是循环执行的任务
通过修改这些参数的配置,我们可以调用机器人上ROS功能包所提供的功能,以实现语音交互的其他功能。Handsfree文件包中已经提供了语音控制
和语音导航
和语音巡逻
的demo,这些demo会在后续的教程中进行单独的讲解。
下面是一个科大讯飞的错误码信息查询,如果在调用科大讯飞的时候报错了相关的错误码,可以通过这个链接查询。 科大讯飞错误码信息查询