最近看一个综艺《向往的生活》被百度的产品小精灵小度所吸引,春招的时候百度来线下宣讲我怎么没举手回答问题拿到这等福利,失手了失手了。本人之前做过一个基于网络通信的Linux聊天室,可能因为做的太挫了,也没人和我用,受这个激发在百度上搜了搜居然可以用小度的文本识别的接口,又找了个语音识别的接口且当自娱自乐就完成了下面这个对话语音小精灵(总算是有人和我聊天了),等有空了接到聊天室里看看好使不。
GIT源码:https://github.com/GreenDaySky/_AIchat
涉及技术:C++ STL、http第三方库、图灵机器人、百度语音识别和语音合成、Linux系统/网络编程、各种第三方库和第三方工具的安装与使用
实现功能:在Linux下和操作系统进行语音对话交流,使其能够和操作者进行语音聊天或者完成一些本地操作
(这个语音对话的实际上不好演示,这里机器人说话的同时也打了字幕)
我的程序是Lewis,启动他以后是我与他的对话,在网络良好的情况下百度语音识别的准确度还是蛮高的。
接下来是我在本地定义的一些关键词启动命令和程序,首先是配置文件(commmand.etc)
没有写很多,如果各位看官愿意扩展喜欢倒置这块还是挺有意思的,这里的a.out是本地编译的一个有趣的打印小程序
笔芯发射
查看当前文件下目录及文件
首先这里我们的程序主体功能基本全封装在了Lewis.hpp这个文件当中,主要有以下几个类提供服务
这个类的主要作用就是和远端图灵机器人进行连接进行本文内容的交互
这是我建立的小机器人Lewis
官网:http://www.turingapi.com/ (关于接口的信息可以上官网查询)
class TuringRT{
private:
//图灵机器人的请求地址
string url = "http://openapi.tuling123.com/openapi/api/v2";
//机器人信息
string api_key = "67a04c4097594cb3bd6c3722b8569524";
//用户id
string user_id = "1";
//这里请求发送的方式采用现成的百度语音识别Http Client
aip::HttpClient client;
public:
TuringRT()
{}
//创造一个图灵机器人接收的Json格式(序列化)
string MakeJsonString(const string &message)
{
Json::Value root;
Json::Value item;
item["apiKey"] = api_key;
item["userId"] = user_id;
root["reqType"] = 0;
root["userInfo"] = item;
Json::Value item1;
item1["text"] = message;
Json::Value item2;
item2["inputText"] = item1;
root["perception"] = item2;
Json::StreamWriterBuilder wb;
ostringstream os;
unique_ptr jw(wb.newStreamWriter());
jw->write(root ,&os);
return os.str();
}
//通过百度语音识别现有的http client请求发起请求
string RequestPost(string &body)
{
string response;
int code = client.post(url, nullptr, body, nullptr, &response);
if(code != CURLcode::CURLE_OK){
return "";
}
return response;
}
//将图灵机器人回复的响应信息拆解拿出(反序列化)
string ParseJson(string &response)
{
JSONCPP_STRING errs;
Json::Value root;
Json::CharReaderBuilder rb;
unique_ptr const rp(rb.newCharReader());
bool res = rp->parse(response.data(),\
response.data() + response.size(), &root, &errs);
if(!res || !errs.empty()){
return "";
}
Json::Value item = root["results"][0];
Json::Value item1 = item["values"];
return item1["text"].asString();
}
//图灵机器人的对话功能
void Talk(string message, string &result)
{
string body = MakeJsonString(message);
//cout << body << endl;
string response = RequestPost(body);
//cout << response << endl;
result = ParseJson(response);
//cout << result << endl;
}
~TuringRT()
{}
};
我们这里就主要利用了百度识别上的一个http的网络接口将采用json序列化的文本信息发送给图灵机器人后台,然后接收到反馈信息之后再将该信息反序列化拆包成我们要的文本信息,实际上也就是这里Talk的功能
该SpeecRec类囊括了该程序的语音部分,主要分为语音识别和语音合成。
语音识别:将我们自己录入的音频信息通过传递给百度语音后台将其翻译为文本信息,以供给图灵机器人平台使用
语音合成:将图灵机器人给我们的反馈文本信息传递给百度语音后台将其合成为音频信息,最终能实现将它在本地播放
//百度语音
class SpeechRec{
private:
//注册百度账号后的账号信息
static string app_id;
static string api_key;
static string secret_key;
//SDK对应的语音识别客户端
aip::Speech *client;
public:
SpeechRec()
{
client = new aip::Speech(app_id, api_key, secret_key);
}
//语音识别
string ASR(const string &voice_bin)
{
Util::BeginShowMessage("正在识别");
//语音识别参数,详见百度文档
map options;
//语言参数:中文
options["dev_pid"] = "1536";
//上传音频文件进行识别
string file_content;
aip::get_file_content(voice_bin.c_str(), &file_content);
Json::Value result = client->recognize(file_content, "wav", \
16000, options);
Util::EndShowMessage();
//识别结果信息判断
int code = result["err_no"].asInt();
if(code != 0){
cerr << "code:" << code << "err_meg:" << \
result["err_msg"].asString() << endl;
return "";
}
return result["result"][0].asString();
}
//语音合成
void TTS(string &text, string voice_tts)
{
ofstream ofile;
string ret;
//设定合成语音信息
map options;
options["spd"] = "5";
options["per"] = "4";
options["vol"] = "15";
Util::BeginShowMessage("正在合成");
ofile.open(voice_tts, ios::out | ios::binary);
Json::Value result = client->text2audio(text, options, ret);
//对合成信息进行判断
if(ret.empty()){
cerr << result.toStyledString() << endl;
}
else{
ofile << ret << endl;
}
ofile.close();
Util::EndShowMessage();
}
~SpeechRec()
{
delete client;
client = nullptr;
}
};
//百度后台对账号信息的识别
string SpeechRec::app_id = "16727887";
string SpeechRec::api_key = "q1Nkfo7UyuCyEHsFTEBmKeWt";
string SpeechRec::secret_key = "b9fNGZ39bnh9OE0Ad4nOcm5Gt8OaAwLf";
传送门:http://ai.baidu.com/
一些服务功能将聚合成了一个类
//服务类
class Util{
private:
static pthread_t tid;
public:
//执行命令并判断是否需要打印信息,不需要则将其输出到垃圾桶
static bool Exec(string command, bool is_print)
{
//将指令附属信息重定向至garbage
if(!is_print){
command += ">/dev/null 2>&1";
}
//popen(comm, type)函数会创造一个管道,再fork一个子进程
//在子进程中执行comm命令,然后stdin/stdot文件指针
FILE *fp = popen(command.c_str(), "r");
if(NULL == fp){
cerr << "popen error" << endl;
return false;
}
if(is_print){
char c;
while(fread(&c, 1, 1, fp) > 0){
cout << c;
}
}
pclose(fp);
return true;
}
static void* Move(void *arg)
{
string message = (char*)arg;
const char *lable = "......";
const char *blank = " ";
const char *x = "|/-\\";
int i = 4;
int j = 0;
while(1){
cout << message << "[" << x[j % 3] << "]" << lable + i << "\r";
fflush(stdout);
i--;
j++;
if(i < 0){
i = 4;
cout << message << "[" << x[j % 3] << "]" << blank << "\r";
}
usleep(500000);
}
}
//启用一个线程来做正在处理信息提示
static void BeginShowMessage(string message)
{
pthread_create(&tid, NULL, Move, (void*)message.c_str());
}
static void EndShowMessage()
{
pthread_cancel(tid);
}
};
pthread_t Util::tid;
最终的功能聚合实现类
//语音助手综合类
class Lewis{
private:
TuringRT rt;
SpeechRec sr;
unordered_map cmd_map;
public:
Lewis()
{}
//录音功能
bool RecordVoice()
{
Util::BeginShowMessage("正在录音");
bool ret = true;
string command = "arecord -t wav -c 1 -r 16000 -d 3 -f S16_LE ";
command += VOICE_FILE;
if(!Util::Exec(command, false)){
cerr << "Record error!" << endl;
ret = false;
}
Util::EndShowMessage();
return ret;
}
//一些本地操作的配置
void loadEtc(const string &etc)
{
//etc文件里存着文字和本地操作命令组成的键值对
ifstream in(etc);
if(!in.is_open()){
cerr << "open error!" << endl;
return;
}
string sep = ":";
char buffer[BSIZE];
while(in.getline(buffer, sizeof(buffer))){
string str = buffer;
size_t pos = str.find(sep);
if(string::npos == pos){
cout << "command etc error" << endl;
continue;
}
string k = str.substr(0, pos);
k+="。";
string v = str.substr(pos + sep.size());
//将这个检查无误的命令加入到命令的map中
cmd_map.insert(make_pair(k, v));
}
in.close();
}
//判断text是否为命令
bool IsCommand(const string &text, string &out_command)
{
auto iter = cmd_map.find(text);
if(iter != cmd_map.end()){
out_command = iter->second;
return true;
}
out_command = "";
return false;
}
//本地录音
void PlayVoice(string voice_file)
{
string cmd = "cvlc --play-and-exit ";
cmd += voice_file;
Util::Exec(cmd, false);
}
void Run()
{
string voice_bin = VOICE_FILE;
//设立一个变量来选择什么时候退出聊天
volatile bool is_quit = false;
string command;
while(!is_quit){
command = "";
if(RecordVoice()){
string text = sr.ASR(voice_bin);
cout << "我# " << text << endl;
if(IsCommand(text, command)){
cout << "本地执行 " << command << endl;
Util::Exec(command, true);
}
else{
string message;
if(text == "退出。"){
message = "欢迎使用,再见";
is_quit = true;
}
else{
rt.Talk(text, message);
cout << "Lewis机器人# " << message << endl;
}
string voice_tts;
sr.TTS(message, VOICE_TTS_FILE);
PlayVoice(VOICE_TTS_FILE);
}
}
}
}
~Lewis()
{}
};
#include "Lewis.hpp"
#include
int main()
{
Lewis one;
//将本地关键字配置录入
one.loadEtc(ETC);
//运行程序
one.Run();
return 0;
}
该项目的难点不在于代码的难度或者是逻辑思考的疏漏,因为实际上我们在该程序中用到的东西都是成品,只需要读懂接口文档信息调用一些接口就可以,但是这些接口的使用所依赖的环境十分复杂,包括上音频录制音频播放这类在Linux不怎么使用的工具,陆陆续续我装了六七个第三方库和第三方工具
装这些工具真的是令人头大,如果你有更好的替代品当然也是最好的