语音转换子服务,用于调用语音识别 SDK,进行语音识别,将语音转为文字后返回给网关。故需要提供以下接口:
- 语音消息的文字转换:客户端进行语音消息的文字转换
模块划分
-
参数/配置文件解析模块:基于
gflags框架直接使用进行参数/配置文件解析。 -
日志模块:基于
spdlog框架封装的模块直接使用进行日志输出。 -
服务注册模块:基于
etcd框架封装的注册模块直接使用进行语音识别子服务的服务注册。 -
rpc服务模块:基于brpc框架搭建rpc服务器。 -
语音识别
SDK模块:基于语音识别平台提供的sdk直接使用,完成语音的识别转文字。
语音子服务搭建流程
- 参数解析(基于
gflags模块)rpc所需信息:当前服务器的地址端口(用于搭建rpc服务器的监听地址信息)- 服务注册所需信息:
- 注册中心的地址端口:用于进行向服务注册中心进行服务注册
- 外部访问的地址端口:当前服务的访问地址(注意服务器绑定与访问地址是不一致的)
- 语音识别平台所需信息:
app id、api key、secret key - 日志模块所需信息:运行模式、日志文件名称、日志输出级别
- 初始化日志模块
- 搭建
RPC服务器:其中包括实现语音识别业务接口功能(具体实现可见环境搭建中的笔记)
- 向注册中心进行服务注册
speech_server.hpp 文件
// 实现语音识别子服务,利用brpc搭建RPC服务,集成ASR模块和服务注册功能
#include <brpc/server.h>
#include <butil/logging.h>
#include "asr.hpp" // 语音识别模块封装(负责调用底层ASR引擎,转录语音数据)
#include "etcd.hpp" // 服务注册模块封装(用于向etcd注册服务,让其他服务可以发现它)
#include "logger.hpp" // 日志模块封装(统一管理日志输出,让调试更轻松)
#include "speech.pb.h" // protobuf框架代码(定义了RPC请求和响应的数据结构)
namespace liren {
// SpeechServiceImpl 类实现了 SpeechService 接口
// 该类负责处理语音识别的RPC调用
class SpeechServiceImpl : public liren::SpeechService
{
public:
// 构造函数:接收一个语音识别客户端ASRClient智能指针,后续用于调用语音识别模块
SpeechServiceImpl(const ASRClient::ptr &asr_client)
: _asr_client(asr_client)
{}
~SpeechServiceImpl(){}
// 实现语音转文字的RPC接口
// 参数说明:
// - controller: RPC调用上下文(用于控制和传递RPC调用信息)
// - request: 客户端发起的语音识别请求,其中包含语音数据和请求ID
// - response: 服务器返回的响应对象,用于保存识别结果或错误信息
// - done: RPC调用结束后的回调函数,确保资源释放
void SpeechRecognition(google::protobuf::RpcController* controller,
const ::liren::SpeechRecognitionReq* request,
::liren::SpeechRecognitionRsp* response,
::google::protobuf::Closure* done)
{
// 记录接收到语音转文字请求的调试日志
LOG_DEBUG("收到语音转文字请求!");
// 使用ClosureGuard确保即使在异常情况下也能正确调用done回调结束RPC调用
brpc::ClosureGuard rpc_guard(done);
// 1. 从请求中提取语音数据,然后调用ASR模块进行识别
std::string err; // 用于保存错误信息
std::string res = _asr_client->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;
}
// 2. 如果识别成功,则构造成功的响应信息
response->set_request_id(request->request_id());
response->set_success(true);
response->set_recognition_result(res);
}
private:
ASRClient::ptr _asr_client; // 保存语音识别模块的客户端对象,用于调用识别接口
};
// SpeechServer 类封装了RPC服务器对象,用于启动并管理整个语音识别服务
class SpeechServer
{
public:
// 定义智能指针别名,方便管理对象生命周期
using ptr = std::shared_ptr<SpeechServer>;
// 构造函数:传入ASR客户端、服务注册客户端和RPC服务器对象
SpeechServer(const ASRClient::ptr asr_client,
const Registry::ptr ®_client,
const std::shared_ptr<brpc::Server> &server)
: _asr_client(asr_client)
, _reg_client(reg_client)
, _rpc_server(server)
{}
~SpeechServer(){}
// 启动RPC服务器,服务将一直运行直到收到退出信号
void start() {
_rpc_server->RunUntilAskedToQuit();
}
private:
ASRClient::ptr _asr_client; // 语音识别客户端对象
Registry::ptr _reg_client; // 服务注册客户端对象
std::shared_ptr<brpc::Server> _rpc_server; // brpc服务器对象
};
// SpeechServerBuilder 类用于分步构建SpeechServer实例,
// 包括创建ASR客户端、服务注册客户端以及RPC服务器
class SpeechServerBuilder
{
public:
// 构造语音识别客户端对象
// 参数:
// - app_id: 应用ID
// - api_key: API密钥
// - secret_key: 秘密密钥
void make_asr_object(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);
}
// 构造服务注册客户端对象,并在注册中心中注册服务
// 参数:
// - reg_host: 注册中心的主机地址
// - service_name: 服务名称(例如"SpeechService")
// - access_host: 当前服务的访问地址
void make_reg_object(const std::string ®_host,
const std::string &service_name,
const std::string &access_host)
{
_reg_client = std::make_shared<Registry>(reg_host);
_reg_client->regiter(service_name, access_host);
}
// 构造并启动RPC服务器
// 参数:
// - port: 监听端口
// - timeout: 空闲连接超时时间(秒)
// - num_threads: 服务器工作线程数
void make_rpc_server(uint16_t port, int32_t timeout, uint8_t num_threads)
{
// 如果ASR客户端尚未初始化,则直接报错退出
if (!_asr_client) {
LOG_ERROR("还未初始化语音识别模块!");
abort();
}
// 创建brpc服务器对象
_rpc_server = std::make_shared<brpc::Server>();
// 实例化语音识别服务实现对象,并将其注册到RPC服务器中
SpeechServiceImpl *speech_service = new SpeechServiceImpl(_asr_client);
int ret = _rpc_server->AddService(speech_service,
brpc::ServiceOwnership::SERVER_OWNS_SERVICE);
if (ret == -1) {
LOG_ERROR("添加Rpc服务失败!");
abort();
}
// 设置服务器选项:包括空闲超时和工作线程数
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();
}
}
// 构建SpeechServer对象,确保所有依赖模块均已正确初始化
SpeechServer::ptr build() {
if (!_asr_client) {
LOG_ERROR("还未初始化语音识别模块!");
abort();
}
if (!_reg_client) {
LOG_ERROR("还未初始化服务注册模块!");
abort();
}
if (!_rpc_server) {
LOG_ERROR("还未初始化RPC服务器模块!");
abort();
}
// 构建并返回封装了所有模块的SpeechServer实例
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; // 存储RPC服务器对象
};
}speech_server.cc 文件
// 主要实现语音识别子服务的服务器的搭建
#include "speech_server.hpp"
DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");
DEFINE_string(registry_host, "http://127.0.0.1:2379", "服务注册中心地址");
DEFINE_string(base_service, "/service", "服务监控根目录");
DEFINE_string(instance_name, "/speech_service/instance", "当前实例名称");
DEFINE_string(access_host, "127.0.0.1:10001", "当前实例的外部访问地址");
DEFINE_int32(listen_port, 10001, "Rpc服务器监听端口");
DEFINE_int32(rpc_timeout, -1, "Rpc调用超时时间");
DEFINE_int32(rpc_threads, 1, "Rpc的IO线程数量");
DEFINE_string(app_id, "117644864", "语音平台应用ID");
DEFINE_string(api_key, "5Bt1BCHIJcJIO2AoMbBsSqC9", "语音平台API密钥");
DEFINE_string(secret_key, "Y1gqrypoZHMWAoh6M20UH88wfbn89uRw", "语音平台加密密钥");
int main(int argc, char *argv[])
{
// 初始化日志
google::ParseCommandLineFlags(&argc, &argv, true);
liren::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);
// 创建语音服务对象
liren::SpeechServerBuilder ssb;
ssb.make_asr_object(FLAGS_app_id, FLAGS_api_key, FLAGS_secret_key);
ssb.make_rpc_server(FLAGS_listen_port, FLAGS_rpc_timeout, FLAGS_rpc_threads);
ssb.make_reg_object(FLAGS_registry_host, FLAGS_base_service + FLAGS_instance_name, FLAGS_access_host);
auto server = ssb.build();
// 启动服务
server->start();
return 0;
}speech.proto 文件
注意下述文件存放在 proto 目录中!
/*
语音识别服务器的子服务注册信息: /service/speech/instance_id
服务名称:/service/speech
实例 ID: instance_id 每个能够提供用户操作服务的子服务器唯一 ID
当服务发现的时候,通过 /service/speech 进行服务发现,就可以发现所有的能够提供用户操作的实例信息了
*/
syntax = "proto3";
package liren;
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);
}CMakeLists.txt 文件
当前子服务的目录如下所示:
# 1. 添加cmake版本说明
cmake_minimum_required(VERSION 3.1.3)
# 2. 声明工程名称
project(speech_server)
set(target "speech_server")
set(test_client "speech_client")
# 3. 检测并生成ODB框架代码
# 1. 添加所需的proto映射代码文件名称
set(proto_path ${CMAKE_CURRENT_SOURCE_DIR}/../proto)
set(proto_files speech.proto)
# 2. 检测框架代码文件是否已经生成
set(proto_hxx "")
set(proto_cxx "")
set(proto_srcs "")
foreach(proto_file ${proto_files})
# 3. 如果没有生成,则预定义生成指令 -- 用于在构建项目之间先生成框架代码
string(REPLACE ".proto" ".pb.cc" proto_cc ${proto_file})
string(REPLACE ".proto" ".pb.h" proto_hh ${proto_file})
if (NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}${proto_cc})
add_custom_command(
PRE_BUILD
COMMAND protoc
ARGS --cpp_out=${CMAKE_CURRENT_BINARY_DIR} -I ${proto_path} --experimental_allow_proto3_optional ${proto_path}/${proto_file}
DEPENDS ${proto_path}/${proto_file}
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${proto_cc}
COMMENT "生成Protobuf框架代码文件:" ${CMAKE_CURRENT_BINARY_DIR}/${proto_cc}
)
endif()
list(APPEND proto_srcs ${CMAKE_CURRENT_BINARY_DIR}/${proto_cc})
endforeach()
# 4. 获取源码目录下的所有源码文件
set(src_files "")
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/source src_files)
# 5. 声明目标及依赖
add_executable(${target} ${src_files} ${proto_srcs})
# 7. 设置需要连接的库
target_link_libraries(${target} -lgflags -lspdlog -lfmt -lbrpc -lssl -lcrypto -lprotobuf -lleveldb -letcd-cpp-api -lcpprest -lcurl /usr/lib/x86_64-linux-gnu/libjsoncpp.so.19)
set(test_files "")
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/test test_files)
add_executable(${test_client} ${test_files} ${proto_srcs})
target_link_libraries(${test_client} -lgflags -lspdlog -lfmt -lbrpc -lssl -lcrypto -lprotobuf -lleveldb -letcd-cpp-api -lcpprest -lcurl /usr/lib/x86_64-linux-gnu/libjsoncpp.so.19)
# 6. 设置头文件默认搜索路径
include_directories(${CMAKE_CURRENT_BINARY_DIR})
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../header)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../third/include)
#8. 设置安装路径
INSTALL(TARGETS ${target} ${test_client} RUNTIME DESTINATION bin)speech_client.cc 文件(服务测试)
// speech_server的测试客户端实现
// 1. 进行服务发现--发现speech_server的服务器节点地址信息并实例化的通信信道
// 2. 读取语音文件数据
// 3. 发起语音识别RPC调用
#include "etcd.hpp"
#include "channel.hpp"
#include <gflags/gflags.h>
#include <thread>
#include "aip-cpp-sdk/speech.h"
#include "speech.pb.h"
DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");
DEFINE_string(etcd_host, "http://127.0.0.1:2379", "服务注册中心地址");
DEFINE_string(base_service, "/service", "服务监控根目录");
DEFINE_string(speech_service, "/service/speech_service", "服务监控根目录");
int main(int argc, char *argv[])
{
google::ParseCommandLineFlags(&argc, &argv, true);
liren::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);
// 1. 先构造Rpc信道管理对象
auto sm = std::make_shared<liren::ServiceManager>();
sm->declared(FLAGS_speech_service);
auto put_cb = std::bind(&liren::ServiceManager::online, sm.get(), std::placeholders::_1, std::placeholders::_2);
auto del_cb = std::bind(&liren::ServiceManager::offline, sm.get(), std::placeholders::_1, std::placeholders::_2);
// 2. 构造服务发现对象
liren::Discovery::ptr dclient = std::make_shared<liren::Discovery>(FLAGS_etcd_host, FLAGS_base_service, put_cb, del_cb);
// 3. 通过Rpc信道管理对象,获取提供Echo服务的信道
auto channel = sm->getChannel(FLAGS_speech_service);
if (!channel) {
std::this_thread::sleep_for(std::chrono::seconds(1));
return -1;
}
// 读取语音文件数据
std::string file_content;
aip::get_file_content("16k.pcm", &file_content);
std::cout << file_content.size() << std::endl;
// 4. 发起EchoRpc调用
liren::SpeechService_Stub stub(channel.get());
liren::SpeechRecognitionReq req;
req.set_speech_content(file_content);
req.set_request_id("111111");
brpc::Controller *cntl = new brpc::Controller();
liren::SpeechRecognitionRsp *rsp = new liren::SpeechRecognitionRsp();
stub.SpeechRecognition(cntl, &req, rsp, nullptr);
if (cntl->Failed() == true) {
std::cout << "Rpc调用失败:" << cntl->ErrorText() << std::endl;
delete cntl;
delete rsp;
std::this_thread::sleep_for(std::chrono::seconds(1));
return -1;
}
if (rsp->success() == false) {
std::cout << rsp->errmsg() << std::endl;
return -1;
}
std::cout << "收到响应: " << rsp->request_id() << std::endl;
std::cout << "收到响应: " << rsp->recognition_result() << std::endl;
return 0;
}注意事项
- 测试时候要将
16k.pcm文件复制一份到创建的build目录中,因为我们用cmake后创建的可执行文件在build中,此时找不到16k.pcm文件的话会无法识别! - 由于
gcc/g++的版本问题,可能出现一些头文件包含、函数参数作用域问题,都可以通过浏览器检索来解决!