WeNet云端推理部署代码解析(下)

举报
xiaoye0829 发表于 2021/12/12 15:00:14 2021/12/12
【摘要】 《WeNet云端推理部署代码解析(上)》一文对WeNet云端推理代码进行探索,介绍了代码结构、前端和解码器部分代码。在本文中,笔者将继续解析WeNet云端部署代码,由于流式ASR服务需要在客户端和服务端之间进行双向的流式数据传输,WeNet实现了两种支持双向流式通信的服务化接口,分别基于WebSocket和gRPC。

《WeNet云端推理部署代码解析(上)》一文对WeNet云端推理代码进行探索,介绍了代码结构、前端和解码器部分代码。在本文中,笔者将继续解析WeNet云端部署代码,由于流式ASR服务需要在客户端和服务端之间进行双向的流式数据传输,WeNet实现了两种支持双向流式通信的服务化接口,分别基于WebSocket和gRPC。

1. 基于WebSocket

1)WebSocket简介

WebSocket是基于TCP的一种新的网络协议,与HTTP协议不同,WebSocket允许服务器主动发送信息给客户端。 在连接建立后,客户端和服务端可以连续互相发送数据,而无需在每次发送数据时重新发起连接请求。因此大大减小了网络带宽的资源消耗 ,在性能上更有优势。

WebSocket支持文本和二进制两种格式的数据传输 。

2)WeNet的WebSocket接口

WeNet使用了boost库的WebSocket实现,定义了WebSocketClient(客户端)和WebSocketServer(服务端)两个类。

在流式ASR过程中,WebSocketClient给WebSocketServer发送数据可以分为三个步骤:1)发送开始信号与解码配置;2)发送二进制语音数据:pcm字节流;3)发送停止信号。从WebSocketClient::SendStartSignal()和WebSocketClient::SendEndSignal()可以看到,开始信号、解码配置和停止信号都是包装在json字符串中,通过WebSocket文本格式传输。pcm字节流则通过WebSocket二进制格式进行传输。

void WebSocketClient::SendStartSignal() {
  // TODO(Binbin Zhang): Add sample rate and other setting surpport
  json::value start_tag = {{"signal", "start"},
                           {"nbest", nbest_},
                           {"continuous_decoding", continuous_decoding_}};
  std::string start_message = json::serialize(start_tag);
  this->SendTextData(start_message);
}

void WebSocketClient::SendEndSignal() {
  json::value end_tag = {{"signal", "end"}};
  std::string end_message = json::serialize(end_tag);
  this->SendTextData(end_message);
}

WebSocketServer在收到数据后,需要先判断收到的数据是文本还是二进制格式:如果是文本数据,则进行json解析,并根据解析结果进行解码配置、启动或停止,处理逻辑定义在ConnectionHandler::OnText()函数中。如果是二进制数据,则进行语音识别,处理逻辑定义在ConnectionHandler::OnSpeechData()中。

3)缺点

WebSocket需要开发者在WebSocketClient和WebSocketServer写好对应的消息构造和解析代码,容易出错。另外,从以上代码来看,服务需要借助json格式来序列化和反序列化数据,效率没有protobuf格式高。

对于这些缺点,gRPC框架提供了更好的解决方法。

2. 基于gRPC

1)gRPC简介

gRPC是谷歌推出的开源RPC框架,使用HTTP2作为网络传输协议,并使用protobuf作为数据交换格式,有更高的数据传输效率。在gRPC框架下,开发者只需通过一个.proto文件定义好RPC服务(service)与消息(message),便可通过gRPC提供的代码生成工具(protoc compiler)自动生成消息构造和解析代码,使开发者能更好地聚焦于接口设计本身。

进行RPC调用时,gRPC Stub(客户端)向gRPC Server(服务端)发送.proto文件中定义的Request消息,gRPC Server在处理完请求之后,通过.proto文件中定义的Response消息将结果返回给gRPC Stub。

gRPC具有跨语言特性,支持不同语言写的微服务进行互动,比如说服务端用C++实现,客户端用Ruby实现。protoc compiler支持12种语言的代码生成。

图1:gRPC Server和gRPC Stub交互[1]

2)WeNet的proto文件

WeNet定义的服务为ASR,包含一个Recognize方法,该方法的输入(Request)、输出(Response)都是流式数据(stream)。在使用protoc compiler编译proto文件后,会得到4个文件:wenet.grpc.pb.h,wenet.grpc.pb.cc,wenet.pb.h,wenet.pb.cc。其中,wenet.pb.h/cc中存储了protobuf数据格式的定义,wenet.grpc.pb.h中存储了gRPC服务端/客户端的定义。通过在代码中包括wenet.pb.h和wenet.grpc.pb.h两个头文件,开发者可以直接使用Request消息和Response消息类,访问其字段。

service ASR {
  rpc Recognize (stream Request) returns (stream Response) {}
}

message Request {

  message DecodeConfig {
    int32 nbest_config = 1;
    bool continuous_decoding_config = 2;
  }

  oneof RequestPayload {
    DecodeConfig decode_config = 1;
    bytes audio_data = 2;
  }
}

message Response {

  message OneBest {
    string sentence = 1;
    repeated OnePiece wordpieces = 2;
  }

  message OnePiece {
    string word = 1;
    int32 start = 2;
    int32 end = 3;
  }

  enum Status {
    ok = 0;
    failed = 1;
  }

  enum Type {
    server_ready = 0;
    partial_result = 1;
    final_result = 2;
    speech_end = 3;
  }

  Status status = 1;
  Type type = 2;
  repeated OneBest nbest = 3;
}

3)WeNet的gRPC实现

WeNet gRPC服务端定义了GrpcServer类,该类继承自wenet.grpc.pb.h中的纯虚基类ASR::Service。

语音识别的入口函数是GrpcServer::Recognize,该函数初始化一个GRPCConnectionHandler实例来进行语音识别,并通过ServerReaderWriter类的stream对象来传递输入输出。

Status GrpcServer::Recognize(ServerContext* context,
                             ServerReaderWriter<Response, Request>* stream) {
  LOG(INFO) << "Get Recognize request" << std::endl;
  auto request = std::make_shared<Request>();
  auto response = std::make_shared<Response>();
  GrpcConnectionHandler handler(stream, request, response, feature_config_,
                                decode_config_, symbol_table_, model_, fst_);
  std::thread t(std::move(handler));
  t.join();
  return Status::OK;
}

WeNet gRPC客户端定义了GrpcClient类。客户端在建立与服务端的连接时需实例化ASR::Stub,并通过ClientReaderWriter类的stream对象,实现双向流式通信。

void GrpcClient::Connect() {
  channel_ = grpc::CreateChannel(host_ + ":" + std::to_string(port_),
                                 grpc::InsecureChannelCredentials());
  stub_ = ASR::NewStub(channel_);
  context_ = std::make_shared<ClientContext>();
  stream_ = stub_->Recognize(context_.get());
  request_ = std::make_shared<Request>();
  response_ = std::make_shared<Response>();
  request_->mutable_decode_config()->set_nbest_config(nbest_);
  request_->mutable_decode_config()->set_continuous_decoding_config(
      continuous_decoding_);
  stream_->Write(*request_);
}

grpc_client_main.cc中,客户端分段传输语音数据,每0.5s进行一次传输,即对于一个采样率为8k的语音文件来说,每次传4000帧数据。为了减小传输数据的大小,提升数据传输速度,先在客户端将float类型转为int16_t,服务端在接受到数据后,再将int16_t转为float。c++中float为32位。

int main(int argc, char *argv[]) {
  ...
  // Send data every 0.5 second
  const float interval = 0.5;
  const int sample_interval = interval * sample_rate;
  for (int start = 0; start < num_sample; start += sample_interval) {
    if (client.done()) {
      break;
    }
    int end = std::min(start + sample_interval, num_sample);
    // Convert to short
    std::vector<int16_t> data;
    data.reserve(end - start);
    for (int j = start; j < end; j++) {
      data.push_back(static_cast<int16_t>(pcm_data[j]));
    }
    // Send PCM data
    client.SendBinaryData(data.data(), data.size() * sizeof(int16_t));
    ...
}

总结

本文主要对WeNet云端部署代码进行解析,介绍了WeNet基于WebSocket和基于gRPC的两种服务化接口。

WeNet代码结构清晰,简洁易用,为语音识别提供了从训练到部署的一套端到端解决方案,大大促进了工业落地效率,是非常值得借鉴学习的语音开源项目。

参考

[1] https://grpc.io/docs/what-is-grpc/introduction/

[2]WeNet: Production First and Production Ready End-to-End Speech Recognition Toolkit

[3]WeNet源码

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。