1. 前言
语音合成技术在生活中用越来越广泛,阅读听书、订单播报、智能硬件、语音导航 很多场景下都加入了语音播报功能。语音合成基于深度神经网络技术,提供高度拟人、流畅自然的语音合成服务,可以模拟出不同人的声音,让应用APP、设备开口说话,还能智能化训练个性语音。
这篇文章就介绍华为云提供的语音合成服务使用方法,利用提供的API接口完成语音合成功能,将合成的语音下载下来。


2. 开通功能
华为云的提供的语音合成,是一种将文本转换成逼真语音的服务。用户通过实时访问和调用API获取语音合成结果,将用户输入的文字合成为音频。通过音色选择、自定义音量、语速,为企业和个人提供个性化的发音服务。
2.1 语音交互服务
地址:https://console.huaweicloud.com/sis/?region=cn-north-4#/sis/stts

2.2 帮助文档
地址: https://support.huaweicloud.com/api-sis/sis_03_0111.html

(1)请求Header参数:
| 参数 | 是否必选 | 参数类型 | 描述 | 
|---|---|---|---|
| X-Auth-Token | 是 | String | 用户Token。Token认证就是在调用API的时候将Token加到请求消息头,从而通过身份认证,获得操作API的权限,响应消息头中X-Subject-Token的值即为Token。 | 
请求头里的X-Auth-Token字段在之前的文章已经介绍过,获取方法看这里: https://bbs.huaweicloud.com/blogs/317759 翻到2.3小节。
(2)请求Body参数:
| 参数 | 是否必选 | 参数类型 | 描述 | 
|---|---|---|---|
| text | 是 | String | 待合成的文本,文本长度限制小于500字符。 | 
| config | 否 | 配置JSON | 语音合成配置信息。 | 
(3)TtsConfig的配置参数:
| 参数 | 是否必选 | 参数类型 | 描述 | 
|---|---|---|---|
| audio_format | 否 | String | 语音格式头:wav、mp3、pcm默认:wav父节点:config | 
| sample_rate | 否 | String | 采样率:16000、8000默认:8000父节点:config | 
| property | 否 | String | 语音合成特征字符串,组成形式为{language}{speaker}{domain},即“语种_人员标识_领域”。发音人分为普通发音人和精品发音人,每次调用价格相同,针对精品发音人,每50字计一次调用,不足50字按一次计;普通发音人每100字计一次调用,不足100字按一次计。其中1个汉字、1个英文字母或1个标点均算作1个字符。精品发音人:区域仅支持cn-north-4、cn-east-3,暂时不支持音高调节。使用精品发音人如果报错SIS.0411,请检查是否符合使用约束。默认:chinese_xiaoyan_common父节点:config | 
| speed | 否 | Integer | 语速。取值范围:-500~500默认值:0父节点:config说明:当取值为“0”时,表示一个成年人正常的语速,约为250字/分钟。设置该值时,语速和数值没有绝对的映射关系。 | 
| pitch | 否 | Integer | 音高。取值范围: -500~500默认值:0父节点:config | 
| volume | 否 | Integer | 音量。取值范围:0~100默认值:50父节点:config | 
(4)普通发音人property取值范围:
| property取值 | 描述 | 
|---|---|
| chinese_xiaoqi_common | 小琪,标准女声发音人。 | 
| chinese_xiaoyu_common | 小宇,标准男声发音人。 | 
| chinese_xiaoyan_common | 小燕,温柔女声发音人。 | 
| chinese_xiaowang_common | 小王,童声发音人。 | 
| chinese_xiaowen_common | 小雯,柔美女声发音人。 | 
| chinese_xiaojing_common | 小婧,俏皮女声发音人。 | 
| chinese_xiaosong_common | 小宋,激昂男声发音人。 | 
| chinese_xiaoxia_common | 小夏,热情女声发音人。 | 
| chinese_xiaodai_common | 小呆,呆萌童声发音人。 | 
| chinese_xiaoqian_common | 小倩,成熟女声发音人。 | 
| english_cameal_common | cameal,柔美女声英文发音人。 | 
(5)精品发音人property取值范围:
| property取值 | 描述 | 
|---|---|
| chinese_huaxiaoxia_common | 华小夏,热情女声发音人。 | 
| chinese_huaxiaogang_common | 华晓刚,利落男声发音人。 | 
| chinese_huaxiaolu_common | 华小璐,知性女声发音人。 | 
| chinese_huaxiaoshu_common | 华小舒,舒缓女声发音人。 | 
| chinese_huaxiaowei_common | 华小唯,嗲柔女声发音人。 | 
| chinese_huaxiaoliang_common | 华小靓,嘹亮女声发音人。 | 
| chinese_huaxiaodong_common | 华晓东,成熟男声发音人。 | 
| chinese_huaxiaoyan_common | 华小颜,严厉女声发音人。 | 
| chinese_huaxiaoxuan_common | 华小萱,台湾女声发音人。 | 
| chinese_huaxiaowen_common | 华小雯,柔美女声发音人。 | 
| chinese_huaxiaoyang_common | 华晓阳,朝气男声发音人。 | 
| chinese_huaxiaomin_common | 华小闽,闽南女声发音人。 | 
| chinese_huanvxia_literature | 华女侠,武侠女生发音人,只支持16k的采样率。 | 
| chinese_huaxiaoxuan_literature | 华晓悬,悬疑男声发音人,只支持16k的采样率。 | 
| chinese_huaxiaomei_common | 华小美,温柔女声发音人。 | 
(6)响应的Body参数
状态码: 200
| 参数 | 是否必选 | 参数类型 | 描述 | 
|---|---|---|---|
| trace_id | 否 | String | 服务内部的令牌,可用于在日志中追溯具体流程。在某些错误情况下可能没有此令牌字符串。 | 
| result | 否 | object | 调用成功表示识别结果,调用失败时无此字段。 | 
(7)CustomResult参数
| 参数 | 是否必选 | 参数类型 | 描述 | 
|---|---|---|---|
| data | 否 | String | 语音数据,以Base64编码格式返回。用户如需生成音频,需要将Base64编码解码成byte数组,再保存为音频,音频格式同“audio_format”参数设置的值,默认为wav格式。 | 
2.3 在线调试接口
通过在线调试接口,可以快速调试接口参数,请求方式,返回结果等信息。
地址: https://apiexplorer.developer.huaweicloud.com/apiexplorer/debug?product=SIS&api=RunTts

还可以在线填入测试参数,进行测试效果。

2.4 请求接口总结
请求地址格式: POST /v1/{project_id}/tts
完整请求地址:
https://sis-ext.cn-north-4.myhuaweicloud.com/v1/0e5957be8a00f53c2fa7c0045e4d8fbf/tts
请求体:
{
 "text": "请注意坐姿",
 "config": {
  "audio_format": "wav",
  "sample_rate": "16000",
  "property": "chinese_xiaoqi_common",
  "speed": 0,
  "pitch": 0,
  "volume": 0
 }
}
请求头:
{
 "X-Auth-Token": "******",
 "Content-Type": "application/json;charset=UTF-8"
}
响应体:
{"result":{"data":xxxxxxxx"}}
这个xxxx就是返回的Base64编码语音数据,可以解码后保存成文件即可。
3. 实现源码
软件采用QT设计的,核心部分主要是用到了HTTP请求相关的操作。


3.1 文字转语音源码
//文本转语音
void Widget::TextToAudio(QString text)
{
    function_select=1;
    QString requestUrl;
    QNetworkRequest request;
    //设置请求地址
    QUrl url;
    //请求地址
    requestUrl = QString("https://sis-ext.%1.myhuaweicloud.com/v1/%2/tts")
            .arg(SERVER_ID)
            .arg(PROJECT_ID);
    //设置数据提交格式
    request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json"));
    //设置token
    request.setRawHeader("X-Auth-Token",Token);
    //构造请求
    url.setUrl(requestUrl);
    request.setUrl(url);
    QString post_param=QString
               ("{"
                "\"text\": \"%1\","
                "\"config\": {"
                 "\"audio_format\": \"%2\","
                 "\"sample_rate\": \"%3\","
                 "\"property\": \"%4\","
                 "\"speed\": %5,"
                 "\"pitch\": 0,"
                 "\"volume\": %6"
                "}"
               "}").arg(text).arg(ui->comboBox_formt->currentText())
            .arg(ui->comboBox_cai_yang_lv->currentText())
            .arg(ui->comboBox_fa_yin_ren->currentText())
            .arg(ui->spinBox_audio_speed->value())
            .arg(ui->spinBox_yin_liang->value());
    //发送请求
    manager->post(request, post_param.toUtf8());
}
//生成语音
void Widget::on_pushButton_to_audio_clicked()
{
    QString text=ui->lineEdit->text();
    if(text.isEmpty())
    {
        QMessageBox::information(this,"提示","请输入文本",
        QMessageBox::Ok,QMessageBox::Ok);
        return;
    }
    qDebug()<<"text:"<<text;
    TextToAudio(text);
}
3.2 获取token
/*
功能: 获取token
*/
void Widget::GetToken()
{
    //表示获取token
    function_select=3;
    QString requestUrl;
    QNetworkRequest request;
    //设置请求地址
    QUrl url;
    //获取token请求地址
    requestUrl = QString("https://iam.%1.myhuaweicloud.com/v3/auth/tokens")
                 .arg(SERVER_ID);
    //自己创建的TCP服务器,测试用
    //requestUrl="http://10.0.0.6:8080";
    //设置数据提交格式
    request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json;charset=UTF-8"));
    //构造请求
    url.setUrl(requestUrl);
    request.setUrl(url);
    QString text =QString("{\"auth\":{\"identity\":{\"methods\":[\"password\"],\"password\":"
    "{\"user\":{\"domain\": {"
    "\"name\":\"%1\"},\"name\": \"%2\",\"password\": \"%3\"}}},"
    "\"scope\":{\"project\":{\"name\":\"%4\"}}}}")
            .arg(MAIN_USER)
            .arg(IAM_USER)
            .arg(IAM_PASSWORD)
            .arg(SERVER_ID);
    //发送请求
    manager->post(request, text.toUtf8());
}
3.3 解析返回值
//解析反馈结果
void Widget::replyFinished(QNetworkReply *reply)
{
    QString displayInfo="";
    int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    //读取所有数据
    QByteArray replyData = reply->readAll();
    qDebug()<<"状态码:"<<statusCode;
    qDebug()<<"反馈的数据:"<<QString(replyData);
    //更新token
    if(function_select==3)
    {
        displayInfo="token 更新失败.";
        //读取HTTP响应头的数据
        QList<QNetworkReply::RawHeaderPair> RawHeader=reply->rawHeaderPairs();
        qDebug()<<"HTTP响应头数量:"<<RawHeader.size();
        for(int i=0;i<RawHeader.size();i++)
        {
            QString first=RawHeader.at(i).first;
            QString second=RawHeader.at(i).second;
            if(first=="X-Subject-Token")
            {
                Token=second.toUtf8();
                displayInfo="token 更新成功.";
                //保存到文件
                SaveDataToFile(Token);
                break;
            }
        }
        QMessageBox::information(this,"提示",displayInfo,
        QMessageBox::Ok,QMessageBox::Ok);
        return;
    }
    //判断状态码
    if(200 != statusCode)
    {
        //解析数据
        QJsonParseError json_error;
        QJsonDocument document = QJsonDocument::fromJson(replyData, &json_error);
        if(json_error.error == QJsonParseError::NoError)
        {
            //判断是否是对象,然后开始解析数据
            if(document.isObject())
            {
                QString error_str="";
                QJsonObject obj = document.object();
                QString error_code;
                //解析错误代码
                if(obj.contains("error_code"))
                {
                    error_code=obj.take("error_code").toString();
                    error_str+="错误代码:";
                    error_str+=error_code;
                    error_str+="\n";
                }
                if(obj.contains("error_msg"))
                {
                    error_str+="错误消息:";
                    error_str+=obj.take("error_msg").toString();
                    error_str+="\n";
                }
                QMessageBox::information(this,"提示",error_str,
                QMessageBox::Ok,QMessageBox::Ok);
            }
         }
        return;
    }
    else if(function_select==1) //语音合成
    {
        //解析数据
        QJsonParseError json_error;
        QJsonDocument document = QJsonDocument::fromJson(replyData, &json_error);
        if(json_error.error == QJsonParseError::NoError)
        {
            //判断是否是对象,然后开始解析数据
            if(document.isObject())
            {
                QJsonObject obj = document.object();
                //解析错误代码
                if(obj.contains("result"))
                {
                    QJsonObject obj1=obj.take("result").toObject();
                    if(obj1.contains("data"))
                    {
                        QString data=obj1.take("data").toString();
                        QByteArray d2=QByteArray::fromBase64(data.toUtf8());
                        qDebug()<<"数据获取成功..";
                        QStringList path_list=QStandardPaths::standardLocations(QStandardPaths::DownloadLocation);
                        //保存到文件
                        QString filename=QFileDialog::getSaveFileName(this,"保存音频文件",path_list.at(0),tr("*.wav *.mp3 *.pcm"));
                        if(filename.isEmpty())
                        {
                            filename=path_list.at(0)+"/123.wmv";
                        }
                        QFile::remove(filename);
                        QFile file_2(filename);
                        file_2.open(QIODevice::WriteOnly);
                        file_2.write(d2); //写入数据
                        file_2.close();
                    }
                }
             }
         }
    }
}









