文章目录

本篇摘要
本文详细介绍了语音识别子服务的全流程实现:从百度智能云获取ASR服务并完成环境搭建与SDK测试,到封装客户端API;再基于BRPC+ETCD设计语音识别子服务,通过Protobuf定义接口,C++实现服务端与客户端,完成语音转文字功能开发与验证,最终成功实现SpeechRecognizeServer。
一.语音识别(asr)服务环境搭建即客户端模块封装
如何获取语音识别服务
下面以百度智能云平台作为服务商来进行语音识别服务的支撑,访问链接:点击速览。
详细步骤:

- 首先进行登录,然后它就会让你实名认证绑定账号,然后登录即可。

- 然后就会来到主页面,这里其实默认平台已经送了你一定额度免费调用语音识别次数。

- 下面创建对应应用。



- 此时就创建了一个应用,下面会有对应的
AppID API Key Secret Key(这三个数据后面调用的时候需要用到,也就是这就是asr客户端进行调用对应asr服务端的凭证)。
此时就完成了调用百度智能云的语音识别服务的凭证创建,后面只需要拿着这些调用对应api进行创建即可。


- 下载对应SDK即可,然后把对应包拖到对应目录即可(后面只需要包含对应的speech.h这个头文件就可以使用)。
安装 sdk 所需依赖:
- 安装jsoncpp开发库
bash
sudo apt install libjsoncpp-dev
- 安装curl工具
bash
sudo apt install curl
- OpenSSL(Ubuntu 22.04默认已安装,无需操作)
bash
# sudo apt install openssl # 注释表示该步骤跳过
注意事项:
- 所有命令需要管理员权限(通过sudo执行)
- 建议按顺序执行前两个安装命令
- 第三个步骤是注释说明,实际不需要运行
- 如果系统没有预装openssl,可取消该行注释手动安装

- 对应百度的SDK包里面内容,只要包含对应base目录下的speech.h即可调用api。
下面把对应短语音-http-SDK的语音包案例下载下方便后续进行识别测试:
https://platform.bj.bcebos.com/sdk/asr/asr_doc/doc_download_files/public.zip(点击即可)

- 解压后可以看到对应内容,下面测试就以16k.pcm为例。
下面查看下对应官方文档C++API的教程:



- 可以看到对应用法,下面就可以参考来进行客户端api书写完成调用(
https://cloud.baidu.com/doc/SPEECH/s/dlbxfrs5o(点击跳转))
通过客户端api实现简单调用
客户端借助服务器调用对应asr-api接口完成向asr服务端调用解析并返回对应文字的流程图:

- 此时对应服务器视角(作为语音识别服务的客户端)就是拿着asr-api接口完成调用,接收应答的过程。
此时我们就已经准备好了对应SDK包(相关的speech.h代码);以及对应测试用例,下面根据官方文档的api格式进行书写测试即可。
测试效果:

- 此时可以看到对应通过asr服务解析出来收到的文字内容和本来这个语音包文件输入进去的语音内容是一样的,故测试成功。
测试代码:
cpp
#include "aip-cpp-sdk-4.16.7/speech.h"
void ASR(aip::Speech *client)
{
std::map<std::string, std::string> options;
// options["dev_pid"] = "1537"; // 普通话识别
std::string file_content;
aip::get_file_content("./16k.pcm", &file_content);
Json::Value result = client->recognize(file_content, "pcm",
16000, options);
std::cout << "语音识别本地文件结果:" << std::endl
<< result.toStyledString();
if (result["err_no"].asInt() != 0)
{
std::cout << result["err_msg"].asString() << std::endl;
return;
}
std::string message = result["result"][0].asString();
std::cout << "message :" << message << std::endl;
}
int main()
{
// 务必替换百度云控制台中新建百度语音应用的 Api Key 和 Secret Key
aip::Speech *client = new aip::Speech("你的App-id", "你的 Api Key ",
"你的 Secret Key");
ASR(client);
// 测试common asr.hpp:
return 0;
}
asr客户端api二次封装
封装思想简述:
- 封装语音识别客户端类,构造函数传入平台key信息,提供接收语音数据并返回转换文字的接口。
asr.hpp代码:
cpp
#pragma once
#include "../ready_example/auto_speech_recognize/aip-cpp-sdk-4.16.7/speech.h"
#include "log.hpp"
class ASRclient
{
public:
using ptr = std::shared_ptr<ASRclient>;
ASRclient(const std::string &app_id,
const std::string &api_key,
const std::string &secret_key) : _client(app_id, api_key, secret_key) {}
std::string Recognize(const std::string &speech_data, std::string &err)
{
std::map<std::string, std::string> options;
Json::Value result = _client.recognize(speech_data, "pcm",
16000, options);
// std::cout << "语音识别本地文件结果:" << std::endl << result.toStyledString();
if (result["err_no"].asInt() != 0)
{
err = result["err_msg"].asString();
return std::string();
}
std::string message = result["result"][0].asString();
return message;
}
private:
aip::Speech _client;
};
- 说白了其实就是照搬下测试用例,稍作修改即可,因为是照搬+修改故测试效果也是相同的。
此时就完成了对应asr-api接口的简单封装,方便后续项目直接可以调用,来提供方便。
二.语音识别子服务(SpeechRecoginzeServer)模块设计及实现
实现主要功能(总体概括):
这个服务是用来把语音变成文字的。用了一个叫做"语音识别SDK"的工具,能把客户发过来的语音消息转成文字,然后把这些文字传回去。这个服务就只有一个主要功能:把语音消息转换成文字。
操作流程图:

操作过程简述:
- 首先向brpc服务端注册对应的语音识别接口的函数服务,然后把对应的brpc服务端,etcd客户端,asr语音识别客户端进行封装,最后向etcd注册完服务,以及brpc完成后就启动对应brpc服务端。
- 然后客户端进行查询etcd注册中心,通过回调函数完成对应信道注册,最后拿着对应信道构建对应brpc请求进行发送语音文件。
- rpc服务端收到后通过封装的asr客户端进行转发给对应的语音识别服务端进行识别后转发回来再次构建rsp发给brpc客户端。
实现过程:
详细整体流程图(纯手绘)

proto文件
proto
syntax = "proto3";
package proto_module;
option cc_generic_services = true;
message SpeechRecognitionReq {
string request_id = 1; //请求ID
bytes speech_content = 2; //语音数据
optional string user_id = 3; //用户ID
optional string session_id = 4; //登录会话ID -- 网关进行身份鉴权
}
message SpeechRecognitionRsp {
string request_id = 1; //请求ID
bool success = 2; //请求处理结果标志
optional string errmsg = 3; //失败原因
optional string recognition_result = 4; //识别后的文字数据
}
//语音识别Rpc服务及接口的定义
service SpeechService {
rpc SpeechRecognition(SpeechRecognitionReq) returns (SpeechRecognitionRsp);
}
- 这里就提供对应的语音解析请求以及应答即可,然后就是对应RPC相关接口的生成对应的proto结构即可(方便后面继承基类实现对接口重写)。
server端实现
首先还是继承对应的之前proto文件写的speechservice的基类,然后完成对应服务函数的重写即可。
代码如下(详细见注释):
cpp
//对应的给rpc注册的服务,也就是收到rpc请求调用的函数
class RpcSpeechService : public proto_module::SpeechService
{
public:
RpcSpeechService(ASRclient::ptr &pclient) : _pclient(pclient)
{
}
// 通过子类外部调用基类对象方式进行调用重写的Echo,私有也能行
void SpeechRecognition(google::protobuf::RpcController *controller,
const ::proto_module::SpeechRecognitionReq *request,
::proto_module::SpeechRecognitionRsp *response,
::google::protobuf::Closure *done)
{
LOG_DEBUG("收到语音转文字请求!");
// 创建一个 brpc::ClosureGuard对象,用于管理 done的生命周期。当 rpc_guard对象析构时,会自动调用 done,通知框架RPC处理完成。
brpc::ClosureGuard rpc_guard(done); // 这里析构的时候调用对应的done->run()构建对应答复信息来通知请求方可以进行自己的回调处理了
std::string err;
std::string res = _pclient->Recognize(request->speech_content(), err);
if (res.empty())
{
LOG_ERROR("{} 语音识别失败!", request->request_id());
response->set_request_id(request->request_id());
response->set_success(false);
response->set_errmsg("语音识别失败:" + err);
return;
}
// 组织响应
response->set_request_id(request->request_id());
response->set_success(true);
response->set_recognition_result(res);
}
private:
ASRclient::ptr _pclient;
// 这里是成员变量不直接在函数里防止每次调用SpeechRecognition都创建_pclient对象麻烦,添加服务时候只需要构建一次对应RpcSpeechService对象
// 然后每次接受到对应请求就去转化通过基类特定接口再调用它的SpeechRecognition接口直接获取成员变量进行语音识别 即可无需每次创建语音识别对象
};
- 这里就是当给brpc服务端进行服务注册,构建对应继承的子类对象给它传进去,然后当客户端发来对应请求时候,它底层内部是一个基类指针,然后拿到的是子类对应的指针,因此访问的就是子类的服务函数(也就是之前重写的),因此就解析请求,进行业务处理然后构建请求,自动发送回去即可。
代码如下:
由于服务端是多模块组合构成的server(如有asr-client模块,etcd-client模块,brpc-server模块等),因此可以采取建造者模式进行一一构建,然后返回对应speechrecognizeserver即可。
cpp
class SpeechServerBuilder
{
public:
using ptr = std::shared_ptr<SpeechServerBuilder>;
SpeechServerBuilder() {}
void make_asr(const std::string &app_id,
const std::string &api_key,
const std::string &secret_key)
{
_asr_client = std::make_shared<ASRclient>(app_id, api_key, secret_key);
}
// brpc里面依赖asr服务,因此先构建对应asr再搭建rpc服务
void make_brpc(uint16_t port, int32_t timeout, uint8_t num_threads)
{
_rpc_server = std::make_shared<brpc::Server>();
// RpcSpeechService rss(_asr_client); 这里局部对象,传给对应brpc-server里面就被析构,保存的地址,此时就出问题了
RpcSpeechService *speech_service = new RpcSpeechService(_asr_client); // 堆上创建,直接传递对应地址即可
int ret = _rpc_server->AddService(speech_service, brpc::ServiceOwnership::SERVER_OWNS_SERVICE); // 告诉brpc-server对象,自己析构的时候来析构speech_service
if (ret == -1)
{
LOG_ERROR("添加Rpc服务失败!");
abort();
}
// 进行rpc服务部署:
brpc::ServerOptions options;
options.idle_timeout_sec = timeout;
options.num_threads = num_threads;
ret = _rpc_server->Start(port, &options);
if (ret == -1)
{
LOG_ERROR("服务启动失败!");
abort();
}
}
void make_reg(const std::string &host, const std::string servervice,const std::string servervice_host )
{
// 往哪里注册:
_reg_client = std::make_shared<Registry>(host);
// 注册什么 :服务名称+对应要部署的ip-port
_reg_client->RegistryDatas(servervice, servervice_host);
}
SpeechServer::ptr build()
{
if (!_asr_client)
{
LOG_ERROR("还未初始化语音识别模块!");
abort();
}
if (!_reg_client)
{
LOG_ERROR("还未初始化服务注册模块!");
abort();
}
if (!_rpc_server)
{
LOG_ERROR("还未初始化RPC服务器模块!");
abort();
}
SpeechServer::ptr server = std::make_shared<SpeechServer>(
_asr_client, _reg_client, _rpc_server);
return server;
}
private:
ASRclient::ptr _asr_client;
Registry::Ptr _reg_client;
std::shared_ptr<brpc::Server> _rpc_server;
};
- 需要注意的细节见注释。
client端实现
实现思路:
- 进行向信道模块设置关心的服务,以及注册对应的回调函数向etcd发现客户端中(当etcd服务端有内容或者内容有变化的话就会进行回调刚刚注册进信道模块中的函数完成对应拿取value进行构建对应rpc服务信道的操作),最后拿着对应信道构建brpc客户端进行对应服务接口函数请求即可。
代码如下(详细见注释):
cpp
// //通过gflags完成对应参数解析:
google::ParseCommandLineFlags(&argc,&argv,true);
//勿忘初始化日志对象:
init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);
// 客户端调用rpc接口通过信道与rpc服务端进行通信
ServiceManager::Ptr sm = std::make_shared<ServiceManager>();
//告诉服务管理者关系哪些服务:
sm->Cared("/service/speech_service");
// 绑定对应etcd监控的服务变化的回调函数(一旦有服务变化的话,对应的回调函数就会完成services的维护工作):
auto put_cb = bind(&ServiceManager::OnlineService, sm.get(), std::placeholders::_1, std::placeholders::_2);
auto del_cb = bind(&ServiceManager::UnonlineService, sm.get(), std::placeholders::_1, std::placeholders::_2);
Discovery::Ptr dis = std::make_shared<Discovery>(FLAGS_etcd_host, FLAGS_base_service, put_cb, del_cb);
auto pchannel = sm->ChooseService("/service/speech_service").get();
if (!pchannel)
{
std::this_thread::sleep_for(std::chrono::seconds(1));
return -1 ;
}
// 构建SpeechService_Stub对象用于向服务端rpc发请求:
proto_module::SpeechService_Stub stub(pchannel);
brpc::Controller *cntl = new brpc::Controller();
proto_module::SpeechRecognitionRsp *rsp = new proto_module::SpeechRecognitionRsp();
proto_module::SpeechRecognitionReq req;
//进行读取对应语音数据进行发送:
std::string content;
aip::get_file_content("16k.pcm",&content);
req.set_speech_content(content);
stub.SpeechRecognition(cntl, &req, rsp, nullptr);
// 接收到rpc的答复就往下走:
if (cntl->Failed() == true)
{
delete cntl;
delete rsp;
LOG_DEBUG("接收应答失败");
return -1 ;
}
std::cout << "收到语音响应内容: " << rsp->recognition_result() << std::endl;
使用CMake进行项目构建
CMakeLists.txt实现思路:
- 收集指定路径下的所有proto文件,进行向构建目录使用命令输出对应的.h / .cc文件(设置对应检查:如果当前目录存在就不用cmake重新构建浪费时间了),然后收集各个目录的.cc 以及对应源文件(sever.cc client.cc等),指定头文件搜索路径,最后对他俩进行一一链接库生成可执行文件(当然了
这里只有一个speech.proto文件,因此就只需要用protoc编译这一个即可)。
CMakeLists.txt文件源码(点击速览)
测试效果展示:

- 当前build构建目录不存在对应的proto的.cc 或者.h 文件因此直接构建出来。

- 进行make 生成对应文件(可以看到生成了对应程序)。

- 存在对应proto依赖文件,再次cmake就不会在打印重新生成proto依赖文件提示了。

- 启动对应语音识别brpc服务。

- 客户端然后把对应的测试音频文件放在对应目录,接着去读取这个文件发送对应brpc语音识别请求,最后收到对应正确应答。
源码汇总:
本篇小结
本文围绕语音识别服务展开,先从百度智能云获取ASR服务并完成SDK环境搭建与测试,随后封装客户端API;再设计语音识别子服务,基于BRPC+ETCD实现服务注册与调用,通过Protobuf定义接口,C++完成服务端与客户端开发,最终测试验证语音转文字功能。