官方其实有一个语音控制功能, 是加装了一个地平线机器人开发平台的智能语音模块实现的。

几个月前OriginBot开发送给我一个语音模块(在此表示感谢),我试过之后感觉还不错,官方也总结了很多优点,我就不再赘述了。

但是我一直想开发一个远程语音控制的功能,利用手机、电脑进行语音识别然后把指令发送给OriginBot,首先这样不受距离限制,不用在小车旁边才能发出指令,再就是语音是要单独买的,据说要差不多100块,还是挺贵的~

我在基于OriginBot的家庭监控方案 1 有提到过,我基于Django开了一个web网站用于展示小车摄像头采集到的图片,下面的内容依然基于这个web网站开发。

简单地描述一下做的几件事情:

  1. 在上面提到的web中开发了一个网页用于收集语音信息,并在浏览器端把语音识别成文字指令
  2. 后端有一个函数接受浏览器传过来的文字指令并判断是不是已知的指令(事先定义了一些具体的指令,只有这些指令能被执行)
  3. 如果是已知的指令,把执行发送给OringinBot执行

目前在第二步和第三步,我只实现了让小车自动导航到某一个地点,也就是调用了通过来实现基于OringinBot的简单自动导航里面的代码, 以后会考虑陆续把官网上语音控制模块的功能都迁移过来。

网站是基于Django开发的,但是博客的主题是OriginBot和机器人相关的,所以我不打算在博客中讲很多Django的知识,有兴趣的同学可以自己去查阅相关资料,网上非常多。对于不想了解web端代码的同学,可以直接从gitee上c吧代码克隆下来即可使用,具体运行方式我在代码仓库的readme中也有介绍到。

接下来我按照上面提到的三件事情来解释代码。

1. 开发了一个网页用于收集语音,并在浏览器端把语音识别成文字指令
网页相关的代码位于home_monitor/templates/voice_control/index.html,

我为了简化语音识别相关的功能,调用了浏览器的webkitSpeechRecognition这个api,目前这个API只有Chrome和Edge这两款浏览器支持,所以大家在使用的时候要注意。

代码如下:

{% load static %}

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>语音转文字示例</title>

    <style>
        .record-button {
            position: absolute;
            left: 50%;
            bottom: 30%;
            transform: translateX(-50%);
            display: flex;
            justify-content: center;
            align-items: center;
            width: 100px;
            height: 100px;
            background-color: #00BFFF;
            border-radius: 50%;
            font-size: 24px;
            color: white;
            cursor: pointer;
            transition: transform 0.2s;
        }

        .record-button.pressed {
            transform: translateX(-50%) scale(0.9);
        }
    </style>

    {% csrf_token %}
</head>

<body>

    <form id="transcriptForm" method="post" action="{% url 'process_transcript' %}">
        {% csrf_token %}
        <input type="hidden" id="transcript" name="transcript">

        <div class="record-button" id="recordButton">按住说话</div>

    </form>

    <script>
        const recordButton = document.getElementById('recordButton');
        const transcriptInput = document.getElementById('transcript');

        const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
        const recognition = new SpeechRecognition();

        recognition.continuous = true;
        recognition.lang = 'zh-CN';

        recognition.onresult = (event) => {
            const transcript = event.results[event.resultIndex][0].transcript;

            transcriptInput.value = transcript;
            transcriptForm.submit();
        }

        recordButton.addEventListener('mousedown', () => {
            recordButton.classList.add('pressed');
            recognition.start();
        });

        recordButton.addEventListener('mouseup', () => {
            recordButton.classList.remove('pressed');
            recognition.stop();
        });

    </script>

</body>

</html>

2. 后端有一个函数接受浏览器传过来的文字指令并判断是不是已知的指令
后端的函数代码位于 /home-monitor/voice_control/views.py,

主要是process_transcript这个函数,从前端接受一个请求,然后获取其中的transcript参数,也就是浏览器识别后的语音指令
代码如下:

from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, HttpResponseRedirect
import io
import base64
import json
import time

from .utils.send_goal import main as goal_coordinate
from .utils.target_point_mapping import TAEGET_POINT_MAPPING


@login_required()
def voice_control_home(request):
    if request.method == 'GET':
        return render(request, 'voice_control/index.html')


def process_transcript(request):
    if request.method == 'POST':
        audio_data = request.POST.get('transcript', None)
        print(audio_data)
        target = TAEGET_POINT_MAPPING.get(audio_data)
        if target:
            goal_coordinate(target)
        else:
            print("没有预先设定的目标点,请检查")
            return HttpResponseRedirect('/voice')
        return HttpResponseRedirect('/voice')
    return JsonResponse({'status': 'error', 'message': '无效的请求方法'})

3. 如果是已知的指令,把执行发送给OringinBot执行
第2步里面的views.py中引用了utils中的send_goal和target_point_mapping,

send_goal就是把目标点发送给小车并使其运行到指定目标点,具体解释参考通过来实现基于OringinBot的简单自动导航,代码上跟原来的博客中所写的有一点区别,我在这里再贴一遍,主要是修改了传参的方式。

import rclpy
from rclpy.node import Node

from rclpy.action import ActionClient
from rclpy.action.server import GoalStatus
from nav2_msgs.action import NavigateToPose


class GoalCoordinate(Node):
    """_summary_

    Args:
        Node (_type_): _description_
    """
    def __init__(self, x, y, w):
        super().__init__('goal_coordinate')

        self._action_client = ActionClient(self, NavigateToPose, 'navigate_to_pose')
        self.send_goal(x, y, w)

    def send_goal(self, x, y, w):
        """
        #TODO: 应该把目标点和姿势改成以参数的形式接收,这边方便外部调用
        """
        goal_msg = NavigateToPose.Goal()
        goal_msg.pose.header.frame_id = 'map'
        goal_msg.pose.pose.position.x = x
        goal_msg.pose.pose.position.y = y
        goal_msg.pose.pose.orientation.w = w

        self.get_logger().info('Sending goal request...')

        self._send_goal_future = self._action_client.send_goal_async(
            goal_msg, feedback_callback=self.feedback_callback
        )

        self._send_goal_future.add_done_callback(self.goal_response_callback)

    def goal_response_callback(self, future):
        goal_handle = future.result()
        if not goal_handle.accepted:
            self.get_logger().error('Goal request rejected by server!')
        else:
            self.get_logger().info('Goal accepted by server, waiting for result')
            self._get_result_future = goal_handle.get_result_async()
            self._get_result_future.add_done_callback(self.result_callback)

    def feedback_callback(self, feedback_message):
        feedback = feedback_message.feedback
        self.get_logger().info(
            f'Remaining Distance from Destination: {feedback.distance_remaining}'
        )

    def result_callback(self, future):
        result = future.result().result
        status = future.result().status
        if status == GoalStatus.STATUS_SUCCEEDED:
            self.get_logger().info('Goal succeeded')
        else:
            self.get_logger().error(f'Failed with status code: {status}')


def main(target=(), args=None):
    rclpy.init(args=args)

    client = GoalCoordinate(target[0], target[1], target[2])

    rclpy.spin(client)


if __name__ == '__main__':
    main()

target_point_mapping则是用于存储白名单指令的,目前使用的是比较简单的明文指令,也就是一定要说出跟事先设定的一模一样的指令,小车才能执行相关的动作,而不是自然语言识别。
代码如下:

# 现在还很简单,只有一条指令,以后可以自行拓展
TAEGET_POINT_MAPPING = {
    "书房": (2.742, -0.173, 0.035),
}

前面有提到,我这里使用浏览器的webkitSpeechRecognition这个API来做语音识别,虽然简单,但也有较大的限制,而且识别准确率不是很高。

所以我也写了另外一套方法,就是浏览器只用来录音,把音频文件发送到后端,然后在后端调用腾讯云的语音识别服务来做语音转文字的工作,这个方法可以极大提供识别准确率,但是复杂度来变高了很多,代码链接如下,

  1. https://gitee.com/yexia553/home-monitor/blob/master/templates/voice_control/index_tencent.html
  2. https://gitee.com/yexia553/home-monitor/blob/master/voice_control/views.py