Laf 结合百度语音技术轻松实现文本与语音之间的互转

技术

一、关于百度语音技术

百度语音技术是百度语音开放业界领先的语音识别、语音合成技术能力,广泛应用于泛阅读、在线教育、音频审核、电话客服、快递物流等多种行业和场景,赋能开发者,让您的产品能“听”会“说”。

picture.image

二、前期准备

1.百度智能云注册

进入百度智能云官网进行注册,地址:https://cloud.baidu.com/

picture.image

2.创建应用

完成注册后登录百度智能云并进入控制台,然后点击左侧菜单【产品服务】-【语音技术】

picture.image

再点击“语音技术”菜单下的【应用列表】-【创建应用】

picture.image

输入“应用名称,勾选“语音技术”、“语音包名”,输入“应用描述”后点击“立即创建”按钮

picture.image

picture.image

创建完成后回到“应用列表”,复制该应用的API Key和 Secret Key并保存

picture.image

另外,在【语音技术】-【概览】界面中可以领取“免费资源”,不同接口会有相应的免费调用次数,如下图:

picture.image

picture.image

三、在 Laf 中接入百度语音技术

登录 Laf 云开发平台,在应用列表中选择一个应用后点击【开发】按钮,进入 Laf 应用开发 IDE

picture.image

1.添加函数

点击左上角【函数列表】处的“+”按钮创建需要使用到的云函数

1)云函数“baidu-api”:用于实现百度语音技术的接口对接

picture.image

2)云函数“upload-file”:文件上传功能的封装

picture.image

3)云函数“store-file”:用于将上传的文件存放到 Laf 云存储中,关于 Laf 云存储的详细说明可点击链接 https://doc.laf.run/guide/oss/ 进行了解

picture.image

2.将上传的文件存放到 Laf 云存储

先在云存储中添加“Bucket”空间,如下图:

picture.image

picture.image

云函数“store-file”完整代码如下:

  
import cloud from '@lafjs/cloud'  
import { GetObjectCommand, S3 } from '@aws-sdk/client-s3';  
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';  
  
//初始化  
const s3Client = new S3({  
 endpoint: process.env.OSS_EXTERNAL_ENDPOINT,  
 region: process.env.OSS_REGION,  
 credentials: {  
 accessKeyId: process.env.OSS_ACCESS_KEY,  
 secretAccessKey: process.env.OSS_ACCESS_SECRET  
 },  
 forcePathStyle: true,  
})  
//存储空间名称,不带 Laf 应用 appid  
const bucketName = 'store-file'  
  
export default async function (ctx: FunctionContext) {  
 const _body = ctx.body;  
 //参数校验  
 if (!_body.type) {  
 return resultData(-1, '参数type不能为空!');  
 }  
  
 switch (_body.type) {  
 case 'storeFile':  
 //存储文件  
 return await storeFile(_body.param);  
 default:  
 return resultData(-1, '请检查参数type是否有误!');  
 }  
}  
  
//存储文件  
async function storeFile(param) {  
 console.log('storeFile', param);  
 const _param = param;  
 //参数校验  
 if (!_param.fileName || !_param.fileBody || !_param.contentType) {  
 return resultData(-1, '参数fileName、fileBody或contentType不能为空!');  
 }  
  
 try {  
 //文件存储  
 const res = await uploadAppFile(_param.fileName, _param.fileBody, _param.contentType);  
 console.log('文件存储结果:', res)  
 if (res && res.$metadata && res.$metadata.httpStatusCode == 200) {  
 //获取文件存储的绝对路径  
 // const fileUrl = await getAppFileUrl(_param.fileName);  
 const bucket = getInternalBucketName();  
 const fileUrl = 'https://' + bucket + '.oss.laf.dev/' + _param.fileName;  
 return resultData(0, '文件存储成功!', {  
 fileUrl: fileUrl,  
 fileName: _param.fileName  
 });  
 }  
 return resultData(-1, '文件存储失败!');  
 }  
 catch (e) {  
 return resultData(-1, '出现异常!', e.message);  
 }  
}  
  
//拼接文件桶名字  
function getInternalBucketName() {  
 const appid = process.env.APP_ID;  
 return `${appid}-${bucketName}`;  
}  
  
//上传文件  
async function uploadAppFile(key, body, contentType) {  
 const bucket = getInternalBucketName();  
 const res = await s3Client  
 .putObject({  
 Bucket: bucket,  
 Key: key,  
 ContentType: contentType,  
 Body: body,  
 })  
 return res;  
}  
  
//获取文件 url  
async function getAppFileUrl(key) {  
 const bucket = getInternalBucketName();  
 const res = await getSignedUrl(s3Client, new GetObjectCommand({  
 Bucket: bucket,  
 Key: key,  
 }));  
 return res;  
}  
  
//删除文件  
async function delAppFileUrl(key) {  
 const bucket = getInternalBucketName()  
 const res = await s3Client.deleteObject({  
 Bucket: bucket,  
 Key: key  
 });  
 return res;  
}  
  
//返回结果数据  
function resultData(code = -1, msg = '', data = null) {  
 return { code, msg, data }  
}

3.文件上传功能的封装

云函数“upload-file”完整代码如下:

  
import cloud from '@lafjs/cloud'  
  
const fs = require("fs")  
  
export default async function (ctx: FunctionContext) {  
 const _body = ctx.body;  
 const _query = ctx.query;  
 const _type = _body.type ? _body.type : _query.type;  
 //参数校验  
 if (!_type) {  
 return resultData(-1, '参数type不能为空!');  
 }  
  
 const _files = ctx.files;  
 switch (_type) {  
 case 'uploadFile':  
 //上传文件   
 return await uploadFile(_files);  
 default:  
 return resultData(-1, '请检查参数type是否有误!');  
 }  
}  
  
//上传文件  
async function uploadFile(files) {  
 console.log('uploadFile->files', files);  
  
 const _files = files;  
 //参数校验  
 if (!_files || _files.length == 0) {  
 return resultData(-1, '未上传文件!');  
 }  
 const fileInfo = _files[0];  
 if (!fileInfo.filename) {  
 return resultData(-1, '文件名称为空!');  
 }  
 if (!fileInfo.mimetype) {  
 return resultData(-1, '文件类型为空!');  
 }  
  
 try {  
 //获取上传文件的对象  
 let fileData = await fs.readFileSync(fileInfo.path);  
 let fileName = 'TempFiles/' + fileInfo.filename;  
 //检测文件是否有后缀名,且后缀名和类型是否匹配  
 let _mimetype = fileInfo.mimetype.split('/');  
 if (fileInfo.filename.split('.').length < 2 && fileInfo.filename.indexOf(_mimetype[1]) < 0) {  
 //如果上传的图片没有后缀名,则在后面追加类型  
 if (_mimetype[0] == 'image') {  
 fileName = fileName + '.' + _mimetype[1];  
 }  
 else {  
 //如果图片没有后缀名,则统一以wav的形式存储  
 fileInfo.mimetype = 'audio/wave';  
 fileName = fileName + '.wav';  
 }  
 }  
  
 //调用云函数存储文件  
 const ret = await cloud.invoke('store-file', {  
 body: {  
 type: 'storeFile',  
 param: {  
 fileName: fileName,  
 fileBody: fileData,  
 contentType: fileInfo.mimetype  
 }  
 }  
 });  
  
 if (ret.code != 0) {  
 return resultData(-1, ret.msg);  
 }  
 //文件类型  
 ret.data.fileType = fileInfo.mimetype;  
 //文件大小  
 ret.data.fileSize = fileInfo.size;  
 return ret;  
 }  
 catch (e) {  
 return resultData(-1, '异常错误:' + e.message);  
 }  
}  
  
//返回结果数据  
function resultData(code = -1, msg = '', data = null) {  
 return { code, msg, data }  
}

4.百度语音技术接口对接

由于百度接口存在“鉴权认证机制”,故在调用接口前需要先获取AccessToken

  
// 配置百度应用API Key和Secret Key  
const apiKey = 'your api key'  
const secretKey = 'your secret key'  
  
// 获取AccessToken  
async function getAccessToken() {  
 let access_token = '';  
 try {  
 await cloud.fetch({  
 url: 'https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=' + apiKey + '&client_secret=' + secretKey,  
 method: 'POST',  
 headers: {  
 'Content-Type': 'application/json',  
 'Accept': 'application/json'  
 }  
 }).then(function (res) {  
 let d = res.data;  
 if (res.status == 200 && d.access_token) {  
 access_token = d.access_token;  
 }  
 else {  
 console.log('获取AccessToken失败!' + d.error_msg);  
 }  
 }).catch(function (err) {  
 console.log('获取AccessToken异常!', err.message);  
 });  
 }  
 catch (e) {  
 console.log('异常错误:' + e.message);  
 }  
 return access_token;  
}

1)短文本转语音

在云函数“baidu-api”主入口中添加如下代码:

  
export default async function (ctx: FunctionContext) {  
 const _body = ctx.body;  
 const _query = ctx.query;  
 const _type = _body.type ? _body.type : _query.type;  
 //参数校验  
 if (!_type) {  
 return resultData(-1, '参数type不能为空!');  
 }  
  
 switch (_type) {  
 case 'shortTextToVoice':  
 //短文本转语音  
      return await shortTextToVoice(_body.param);  
 default:  
 return resultData(-1, '请检查参数type是否有误!');  
 }  
}  
  
//返回结果数据  
function resultData(code = -1, msg = '', data = null) {  
 return { code, msg, data }  
}

短文本转语音(方法“shortTextToVoice”)具体实现如下:

  
//短文本转语音  
async function shortTextToVoice(param) {  
 console.log('shortTextToVoice', param);  
 const _param = param;  
 //参数校验  
 if (!_param.text) {  
 return resultData(-1, '参数text不能为空!');  
 }  
 if (_param.text.length > 60) {  
 return resultData(-1, '不能超过60个汉字或者字母数字!');  
 }  
  
 const access_token = await getAccessToken();  
 if (!access_token) {  
 return resultData(-1, 'AccessToken获取失败!');  
 }  
  
 try {  
    let _text = _param.text;  
    console.log('shortTextToVoice-->text编码后:', _text);  
 let _cuid = Math.ceil(Math.random() * 1000000000000);  
  
 let obj = null;  
 await cloud.fetch({  
 url: 'https://tsn.baidu.com/text2audio',  
 method: 'POST',  
 headers: {  
 'Content-Type': 'application/x-www-form-urlencoded',  
 'Accept': '*/*'  
 },  
 data: {  
 'tex': _text, //合成的文本,使用UTF-8编码。不超过60个汉字或者字母数字。  
 'tok': access_token, //开放平台获取到的开发者access_token  
 'cuid': _cuid, //用户唯一标识,用来计算UV值。建议填写能区分用户的机器 MAC 地址或 IMEI 码,长度为60字符以内  
 'ctp': '1', //客户端类型选择,web端填写固定值1  
 'lan': 'zh', //固定值zh。语言选择,目前只有中英文混合模式,填写固定值zh  
 'spd': _param.spd ? _param.spd : '5', //语速,取值0-15,默认为5中语速  
 'pit': _param.pit ? _param.pit : '5', //音调,取值0-15,默认为5中语调  
 'vol': _param.vol ? _param.vol : '5', //音量,取值0-15,默认为5中音量(取值为0时为音量最小值,并非为无声)  
 'per': _param.per ? _param.per : '1', //基础音库:度小宇=1,度小美=0,度逍遥(基础)=3,度丫丫=4;精品音库:度逍遥(精品)=5003,度小鹿=5118,度博文=106,度小童=110,度小萌=111,度米朵=103,度小娇=5  
 'aue': _param.aue ? _param.aue : '3' //3为mp3格式(默认);4为pcm-16k;5为pcm-8k;6为wav(内容同pcm-16k); 注意aue=4或者6是语音识别要求的格式,但是音频内容不是语音识别要求的自然人发音,所以识别效果会受影响。  
 },  
 responseType: 'stream',  
 }).then(function (res) {  
 let content_type = res.headers['content-type'];  
 if (res.status == 200 && content_type && content_type.toLowerCase().indexOf('audio') > -1) {  
 obj = resultData(0, '成功!', res.data);  
 }  
 else {  
 obj = resultData(-1, '语音合成失败,err_detail:' + res.data.err_detail);  
 }  
 }).catch(function (err) {  
 console.log('短文本转语音异常!', err.message);  
 obj = resultData(-1, '语音合成异常:' + err.message);  
 });  
  
 //语音合成成功,存储文件  
 if (obj.code == 0) {  
 let fileName = 'TextToVoice/' + _cuid;  
 let _aue = _param.aue ? _param.aue : '3';  
 switch (_aue) {  
 case '4':  
 case '5':  
 fileName = fileName + '.pcm';  
 break;  
 case '6':  
 fileName = fileName + '.wav';  
 break;  
 default:  
 fileName = fileName + '.mp3';  
 break;  
 }  
 //调用云函数存储文件  
 const ret = await cloud.invoke('store-file', {  
 body: {  
 type: 'storeFile',  
 param: {  
 fileName: fileName,  
 fileBody: obj.data,  
 contentType: 'application/octet-stream'  
 }  
 }  
 });  
 if (ret.code == 0) {  
 obj = resultData(0, '语音合成成功!', ret.data);  
 }  
 else {  
 obj = resultData(-1, ret.msg);  
 }  
 }  
 return obj;  
 }  
 catch (e) {  
 return resultData(-1, '异常错误:' + e.message);  
 }  
}

当文本成功转换成语音后,会将音频文件存放到 Laf 云存储中。使用Apipost调用如下:

  
{  
 "type": "shortTextToVoice",  
 "param": {  
 "text": "今天五月初五端午节,祝大家端午节安康!"  
 }  
}

picture.image

picture.image

2)长文本转语音

由于长文本和短文本不一样,当内容过多之后不能实时的返回音频文件,故需要先创建任务,然后再通过任务ID去查询生成的音频文件

在云函数“baidu-api”主入口中增加“长文本转语音-创建任务”和“长文本转语音-查询任务结果”,具体代码如下:

  
export default async function (ctx: FunctionContext) {  
 const _body = ctx.body;  
 const _query = ctx.query;  
 const _type = _body.type ? _body.type : _query.type;  
 //参数校验  
 if (!_type) {  
 return resultData(-1, '参数type不能为空!');  
 }  
  
 switch (_type) {  
 case 'shortTextToVoice':  
 //短文本转语音  
 return await shortTextToVoice(_body.param);  
 case 'longTextToVoice':  
 //长文本转语音-创建任务  
 return await longTextToVoice(_body.param);  
 case 'searchTextToVoice':  
 //长文本转语音-查询任务结果  
      return await searchTextToVoice(_body.param);  
 default:  
 return resultData(-1, '请检查参数type是否有误!');  
 }  
}

长文本转语音-创建任务(方法“longTextToVoice”)具体实现如下:

  
//长文本转语音-创建任务  
async function longTextToVoice(param) {  
 console.log('longTextToVoice', param);  
 const _param = param;  
 //参数校验  
 if (!_param.text || _param.text.length == 0) {  
 return resultData(-1, '参数text不能为空!');  
 }  
  
 const access_token = await getAccessToken();  
 if (!access_token) {  
 return resultData(-1, 'AccessToken获取失败!');  
 }  
  
 try {  
 let obj = null;  
 await cloud.fetch({  
 url: 'https://aip.baidubce.com/rpc/2.0/tts/v1/create?access_token=' + access_token,  
 method: 'POST',  
 headers: {  
 'Content-Type': 'application/json',  
 'Accept': 'application/json'  
 },  
 data: {  
 'text': _param.text, //待合成的文本,需要为UTF-8编码;输入多段文本时,文本间会插入1s长度的空白间隔。总字数不超过10万个字符,1个中文字、英文字母、数字或符号均算作1个字符  
 'format': _param.format ? _param.format : 'mp3-16k', //音频格式:'mp3-16k','mp3-48k','wav','pcm-8k','pcm-16k',默认为mp3-16k  
 'voice': _param.voice ? _param.voice : 0, //基础音库:度小宇=1,度小美=0,度逍遥(基础)=3,度丫丫=4;精品音库:度逍遥(精品)=5003,度小鹿=5118,度博文=106,度小童=110,度小萌=111,度米朵=103,度小娇=5。默认为度小美  
 'lang': 'zh', //固定值zh。语言选择,目前只有中英文混合模式,填写固定值zh  
 'speed': _param.speed ? _param.speed : 5, //语速,取值0-15,默认为5中语速  
 'pitch': _param.pitch ? _param.pitch : 5, //音调,取值0-15,默认为5中语调  
 'volume': _param.volume ? _param.volume : 5, //音量,取值0-15,默认为5中音量(取值为0时为音量最小值,并非为无声)  
 'enable_subtitle': _param.enable_subtitle ? _param.enable_subtitle : '0', //是否开启字幕:取值范围0, 1, 2,默认为0。0表示不开启字幕,1表示开启句级别字幕,2表示开启词级别字幕  
 }  
 }).then(function (res) {  
 let d = res.data;  
 if (res.status == 200 && d.task_id) {  
 obj = resultData(0, '长文本转语音成功!', d);  
 }  
 else {  
 obj = resultData(-1, '长文本转语音失败!' + d.error_msg);  
 }  
 }).catch(function (err) {  
 console.log('长文本转语音异常!', err.message);  
 obj = resultData(-1, '长文本转语音异常:' + err.message);  
 });  
  
 return obj;  
 }  
 catch (e) {  
 return resultData(-1, '异常错误:' + e.message);  
 }  
}

使用Apipost调用结果如下:

  
{  
 "type": "longTextToVoice",  
 "param": {  
 "text": ["今天五月初五端午节","祝大家端午节安康!"]  
 }  
}

picture.image

长文本转语音-查询任务结果(方法“searchTextToVoice”)具体实现如下:

  
//长文本转语音-查询任务结果  
async function searchTextToVoice(param) {  
 console.log('searchTextToVoice', param);  
 const _param = param;  
 //参数校验  
 if (!_param.taskIds || _param.taskIds == 0) {  
 return resultData(-1, '参数taskIds不能为空!');  
 }  
  
 const access_token = await getAccessToken();  
 if (!access_token) {  
 return resultData(-1, 'AccessToken获取失败!');  
 }  
  
 try {  
 let obj = null;  
 //长文本转语音-查询任务  
 await cloud.fetch({  
 url: 'https://aip.baidubce.com/rpc/2.0/tts/v1/query?access_token=' + access_token,  
 method: 'POST',  
 headers: {  
 'Content-Type': 'application/json',  
 'Accept': 'application/json'  
 },  
 data: {  
 'task_ids': _param.taskIds, //任务id,推荐一次查询多个任务id,单次最多可查询200个  
 }  
 }).then(function (res) {  
 let d = res.data;  
 if (res.status == 200 && d.tasks_info) {  
 obj = resultData(0, '长文本转语音-查询成功!', d.tasks_info);  
 }  
 else {  
 obj = resultData(-1, '长文本转语音-查询失败!' + d.error_msg);  
 }  
 }).catch(function (err) {  
 console.log('长文本转语音-查询异常!', err.message);  
 obj = resultData(-1, '长文本转语音-查询异常:' + err.message);  
 });  
  
 return obj;  
 }  
 catch (e) {  
 return resultData(-1, '异常错误:' + e.message);  
 }  
}

使用Apipost调用结果如下:

  
{  
 "type": "searchTextToVoice",  
 "param": {  
 "taskIds": ["6480b88bcde4bf0001132035"]  
 }  
}

picture.image

3) 音频转写

由于音频文件一般比较大,且上传之后需要时间解析,故需要先单独将音频文件上传,然后通过返回的文件url去调用创建任务接口,最后再通过任务ID去查询生成的文本内容

使用Apipost调用云函数“upload-file”接口结果如下:

picture.image

picture.image

在云函数“baidu-api”主入口中增加“音频转写-创建任务”和“音频转写-查询任务结果”,具体代码如下:

  
export default async function (ctx: FunctionContext) {  
 const _body = ctx.body;  
 const _query = ctx.query;  
 const _type = _body.type ? _body.type : _query.type;  
 //参数校验  
 if (!_type) {  
 return resultData(-1, '参数type不能为空!');  
 }  
  
 switch (_type) {  
 case 'shortTextToVoice':  
 //短文本转语音  
 return await shortTextToVoice(_body.param);  
 case 'longTextToVoice':  
 //长文本转语音-创建任务  
 return await longTextToVoice(_body.param);  
 case 'searchTextToVoice':  
 //长文本转语音-查询任务结果  
 return await searchTextToVoice(_body.param);  
 case 'createVoiceToText':  
 //音频转写-创建任务   
 return await createVoiceToText(_body.param);  
 case 'searchVoiceToText':  
 //音频转写-查询任务结果  
 return await searchVoiceToText(_body.param);  
 default:  
 return resultData(-1, '请检查参数type是否有误!');  
 }  
}

长文本转语音-创建任务(方法“longTextToVoice”)具体实现如下:

  
//音频转写-创建任务  
async function createVoiceToText(param) {  
 console.log('voiceToText->param', param);  
 const _param = param;  
 if (!_param.fileUrl || !_param.fileType || _param.fileType.toLowerCase().indexOf('audio') < 0) {  
 return resultData(-1, '请上传音频文件!');  
 }  
 if (!_param.fileName) {  
 return resultData(-1, '文件名称不能为空');  
 }  
 const _format = ['mp3', 'wav', 'pcm', 'm4a', 'amr'];  
 let _fileName = _param.fileName.toLowerCase().split('.');  
 if (_format.indexOf(_fileName[1]) < 0) {  
 return resultData(-1, '仅支持mp3、wav、pcm、m4a、amr格式的音频文件!');  
 }  
 const limitSize = 50 * 1024 * 1024;  
 if (!_param.fileSize || _param.fileSize > limitSize) {  
 return resultData(-1, '音频文件大小不能超过50M!');  
 }  
 const access_token = await getAccessToken();  
 if (!access_token) {  
 return resultData(-1, 'AccessToken获取失败!');  
 }  
  
 try {  
 //文件格式  
 let fileFormat = _param.fileName.split('.')[1];  
 let obj = null;  
 await cloud.fetch({  
 url: 'https://aip.baidubce.com/rpc/2.0/aasr/v1/create?access_token=' + access_token,  
 method: 'POST',  
 headers: {  
 'Content-Type': 'application/json',  
 'Accept': 'application/json'  
 },  
 data: {  
 'speech_url': _param.fileUrl, //音频url,云端可外网访问的url链接,音频大小不超过500MB  
 'format': fileFormat, //音频格式,['mp3', 'wav', 'pcm','m4a','amr']单声道,编码 16bits 位深  
 'pid': _param && _param.pid ? _param.pid : 80001, //语言类型,[80001(中文语音近场识别模型极速版), 80006(中文音视频字幕模型,1737(英文模型)]  
 'rate': 16000 //采样率,[16000] 固定值  
 }  
 }).then(function (res) {  
 let d = res.data;  
 if (res.status == 200 && d.task_id) {  
 obj = resultData(0, '音频转写成功!', d);  
 }  
 else {  
 obj = resultData(-1, '音频转写失败!' + d.error_msg);  
 }  
 }).catch(function (err) {  
 console.log('音频转写异常!', err.message);  
 obj = resultData(-1, '音频转写异常:' + err.message);  
 });  
  
 return obj;  
 }  
 catch (e) {  
 return resultData(-1, '异常错误:' + e.message);  
 }  
}

使用Apipost调用结果如下:

  
{  
 "type": "createVoiceToText",  
 "param": {  
 "fileUrl": "https://l1ygg3-store-file.oss.laf.dev/TempFiles/96bcdd36-373a-4031-b843-33d45c17dc03.mp3",  
 "fileName": "TempFiles/96bcdd36-373a-4031-b843-33d45c17dc03.mp3",  
 "fileType": "audio/mpeg",  
 "fileSize": 1001504  
 }  
}

picture.image

长文本转语音-查询任务结果(方法“searchTextToVoice”)具体实现如下:

  
//音频转写-查询任务结果  
async function searchVoiceToText(param) {  
 console.log('searchVoiceToText', param);  
 const _param = param;  
 //参数校验  
 if (!_param.taskIds || _param.taskIds.length == 0) {  
 return resultData(-1, '参数taskIds不能为空!');  
 }  
  
 const access_token = await getAccessToken();  
 if (!access_token) {  
 return resultData(-1, 'AccessToken获取失败!');  
 }  
  
 try {  
 let obj = null;  
 //音频转写-查询任务  
 await cloud.fetch({  
 url: 'https://aip.baidubce.com/rpc/2.0/aasr/v1/query?access_token=' + access_token,  
 method: 'POST',  
 headers: {  
 'Content-Type': 'application/json',  
 'Accept': 'application/json'  
 },  
 data: {  
 'task_ids': _param.taskIds, //任务id,推荐一次查询多个任务id,单次最多可查询200个  
 }  
 }).then(function (res) {  
 let d = res.data;  
 if (res.status == 200 && d.tasks_info) {  
 obj = resultData(0, '音频转写-查询成功!', d.tasks_info);  
 }  
 else {  
 obj = resultData(-1, '音频转写-查询失败!' + d.error_msg);  
 }  
 }).catch(function (err) {  
 console.log('音频转写-查询异常!', err.message);  
 obj = resultData(-1, '音频转写-查询异常:' + err.message);  
 });  
  
 return obj;  
 }  
 catch (e) {  
 return resultData(-1, '异常错误:' + e.message);  
 }  
}

使用Apipost调用结果如下:

  
{  
 "type": "searchVoiceToText",  
 "param": {  
 "taskIds": ["649469dd3157480001d81a28"]  
 }  
}

picture.image

以上便是 Laf 结合百度语音技术实现文本与语音之间的互转示例。

最后,云函数“baidu-api”完整代码如下:

  
import cloud from '@lafjs/cloud'  
  
// 配置百度应用API Key和Secret Key  
const apiKey = 'your api key'  
const secretKey = 'your secret key'  
  
export default async function (ctx: FunctionContext) {  
 const _body = ctx.body;  
 const _query = ctx.query;  
 const _type = _body.type ? _body.type : _query.type;  
 //参数校验  
 if (!_type) {  
 return resultData(-1, '参数type不能为空!');  
 }  
  
 switch (_type) {  
 case 'shortTextToVoice':  
 //短文本转语音  
 return await shortTextToVoice(_body.param);  
 case 'longTextToVoice':  
 //长文本转语音-创建任务  
 return await longTextToVoice(_body.param);  
 case 'searchTextToVoice':  
 //长文本转语音-查询任务结果  
 return await searchTextToVoice(_body.param);  
 case 'createVoiceToText':  
 //音频转写-创建任务   
 return await createVoiceToText(_body.param);  
 case 'searchVoiceToText':  
 //音频转写-查询任务结果  
 return await searchVoiceToText(_body.param);  
 default:  
 return resultData(-1, '请检查参数type是否有误!');  
 }  
}  
  
//短文本转语音  
async function shortTextToVoice(param) {  
 console.log('shortTextToVoice', param);  
 const _param = param;  
 //参数校验  
 if (!_param.text) {  
 return resultData(-1, '参数text不能为空!');  
 }  
 if (_param.text.length > 60) {  
 return resultData(-1, '不能超过60个汉字或者字母数字!');  
 }  
  
 const access_token = await getAccessToken();  
 if (!access_token) {  
 return resultData(-1, 'AccessToken获取失败!');  
 }  
  
 try {  
    let _text = _param.text;  
 console.log('shortTextToVoice-->text编码后:', _text);  
 let _cuid = Math.ceil(Math.random() * 1000000000000);  
  
 let obj = null;  
 await cloud.fetch({  
 url: 'https://tsn.baidu.com/text2audio',  
 method: 'POST',  
 headers: {  
 'Content-Type': 'application/x-www-form-urlencoded',  
 'Accept': '*/*'  
 },  
 data: {  
 'tex': _text, //合成的文本,使用UTF-8编码。不超过60个汉字或者字母数字。  
 'tok': access_token, //开放平台获取到的开发者access_token  
 'cuid': _cuid, //用户唯一标识,用来计算UV值。建议填写能区分用户的机器 MAC 地址或 IMEI 码,长度为60字符以内  
 'ctp': '1', //客户端类型选择,web端填写固定值1  
 'lan': 'zh', //固定值zh。语言选择,目前只有中英文混合模式,填写固定值zh  
 'spd': _param.spd ? _param.spd : '5', //语速,取值0-15,默认为5中语速  
 'pit': _param.pit ? _param.pit : '5', //音调,取值0-15,默认为5中语调  
 'vol': _param.vol ? _param.vol : '5', //音量,取值0-15,默认为5中音量(取值为0时为音量最小值,并非为无声)  
 'per': _param.per ? _param.per : '1', //基础音库:度小宇=1,度小美=0,度逍遥(基础)=3,度丫丫=4;精品音库:度逍遥(精品)=5003,度小鹿=5118,度博文=106,度小童=110,度小萌=111,度米朵=103,度小娇=5  
 'aue': _param.aue ? _param.aue : '3' //3为mp3格式(默认);4为pcm-16k;5为pcm-8k;6为wav(内容同pcm-16k); 注意aue=4或者6是语音识别要求的格式,但是音频内容不是语音识别要求的自然人发音,所以识别效果会受影响。  
 },  
 responseType: 'stream',  
 }).then(function (res) {  
 let content_type = res.headers['content-type'];  
 if (res.status == 200 && content_type && content_type.toLowerCase().indexOf('audio') > -1) {  
 obj = resultData(0, '成功!', res.data);  
 }  
 else {  
 obj = resultData(-1, '语音合成失败,err_detail:' + res.data.err_detail);  
 }  
 }).catch(function (err) {  
 console.log('短文本转语音异常!', err.message);  
 obj = resultData(-1, '语音合成异常:' + err.message);  
 });  
  
 //语音合成成功,存储文件  
 if (obj.code == 0) {  
 let fileName = 'TextToVoice/' + _cuid;  
 let _aue = _param.aue ? _param.aue : '3';  
 switch (_aue) {  
 case '4':  
 case '5':  
 fileName = fileName + '.pcm';  
 break;  
 case '6':  
 fileName = fileName + '.wav';  
 break;  
 default:  
 fileName = fileName + '.mp3';  
 break;  
 }  
 //调用云函数存储文件  
 const ret = await cloud.invoke('store-file', {  
 body: {  
 type: 'storeFile',  
 param: {  
 fileName: fileName,  
 fileBody: obj.data,  
 contentType: 'application/octet-stream'  
 }  
 }  
 });  
 if (ret.code == 0) {  
 obj = resultData(0, '语音合成成功!', ret.data);  
 }  
 else {  
 obj = resultData(-1, ret.msg);  
 }  
 }  
 return obj;  
 }  
 catch (e) {  
 return resultData(-1, '异常错误:' + e.message);  
 }  
}  
  
//长文本转语音-创建任务  
async function longTextToVoice(param) {  
 console.log('longTextToVoice', param);  
 const _param = param;  
 //参数校验  
 if (!_param.text || _param.text.length == 0) {  
 return resultData(-1, '参数text不能为空!');  
 }  
  
 const access_token = await getAccessToken();  
 if (!access_token) {  
 return resultData(-1, 'AccessToken获取失败!');  
 }  
  
 try {  
 let obj = null;  
 await cloud.fetch({  
 url: 'https://aip.baidubce.com/rpc/2.0/tts/v1/create?access_token=' + access_token,  
 method: 'POST',  
 headers: {  
 'Content-Type': 'application/json',  
 'Accept': 'application/json'  
 },  
 data: {  
 'text': _param.text, //待合成的文本,需要为UTF-8编码;输入多段文本时,文本间会插入1s长度的空白间隔。总字数不超过10万个字符,1个中文字、英文字母、数字或符号均算作1个字符  
 'format': _param.format ? _param.format : 'mp3-16k', //音频格式:'mp3-16k','mp3-48k','wav','pcm-8k','pcm-16k',默认为mp3-16k  
 'voice': _param.voice ? _param.voice : 0, //基础音库:度小宇=1,度小美=0,度逍遥(基础)=3,度丫丫=4;精品音库:度逍遥(精品)=5003,度小鹿=5118,度博文=106,度小童=110,度小萌=111,度米朵=103,度小娇=5。默认为度小美  
 'lang': 'zh', //固定值zh。语言选择,目前只有中英文混合模式,填写固定值zh  
 'speed': _param.speed ? _param.speed : 5, //语速,取值0-15,默认为5中语速  
 'pitch': _param.pitch ? _param.pitch : 5, //音调,取值0-15,默认为5中语调  
 'volume': _param.volume ? _param.volume : 5, //音量,取值0-15,默认为5中音量(取值为0时为音量最小值,并非为无声)  
 'enable_subtitle': _param.enable_subtitle ? _param.enable_subtitle : '0', //是否开启字幕:取值范围0, 1, 2,默认为0。0表示不开启字幕,1表示开启句级别字幕,2表示开启词级别字幕  
 }  
 }).then(function (res) {  
 let d = res.data;  
 if (res.status == 200 && d.task_id) {  
 obj = resultData(0, '长文本转语音成功!', d);  
 }  
 else {  
 obj = resultData(-1, '长文本转语音失败!' + d.error_msg);  
 }  
 }).catch(function (err) {  
 console.log('长文本转语音异常!', err.message);  
 obj = resultData(-1, '长文本转语音异常:' + err.message);  
 });  
  
 return obj;  
 }  
 catch (e) {  
 return resultData(-1, '异常错误:' + e.message);  
 }  
}  
  
//长文本转语音-查询任务结果  
async function searchTextToVoice(param) {  
 console.log('searchTextToVoice', param);  
 const _param = param;  
 //参数校验  
 if (!_param.taskIds || _param.taskIds == 0) {  
 return resultData(-1, '参数taskIds不能为空!');  
 }  
  
 const access_token = await getAccessToken();  
 if (!access_token) {  
 return resultData(-1, 'AccessToken获取失败!');  
 }  
  
 try {  
 let obj = null;  
 //长文本转语音-查询任务  
 await cloud.fetch({  
 url: 'https://aip.baidubce.com/rpc/2.0/tts/v1/query?access_token=' + access_token,  
 method: 'POST',  
 headers: {  
 'Content-Type': 'application/json',  
 'Accept': 'application/json'  
 },  
 data: {  
 'task_ids': _param.taskIds, //任务id,推荐一次查询多个任务id,单次最多可查询200个  
 }  
 }).then(function (res) {  
 let d = res.data;  
 if (res.status == 200 && d.tasks_info) {  
 obj = resultData(0, '长文本转语音-查询成功!', d.tasks_info);  
 }  
 else {  
 obj = resultData(-1, '长文本转语音-查询失败!' + d.error_msg);  
 }  
 }).catch(function (err) {  
 console.log('长文本转语音-查询异常!', err.message);  
 obj = resultData(-1, '长文本转语音-查询异常:' + err.message);  
 });  
  
 return obj;  
 }  
 catch (e) {  
 return resultData(-1, '异常错误:' + e.message);  
 }  
}  
  
//音频转写-创建任务  
async function createVoiceToText(param) {  
 console.log('voiceToText->param', param);  
 const _param = param;  
 if (!_param.fileUrl || !_param.fileType || _param.fileType.toLowerCase().indexOf('audio') < 0) {  
 return resultData(-1, '请上传音频文件!');  
 }  
 if (!_param.fileName) {  
 return resultData(-1, '文件名称不能为空');  
 }  
 const _format = ['mp3', 'wav', 'pcm', 'm4a', 'amr'];  
 let _fileName = _param.fileName.toLowerCase().split('.');  
 if (_format.indexOf(_fileName[1]) < 0) {  
 return resultData(-1, '仅支持mp3、wav、pcm、m4a、amr格式的音频文件!');  
 }  
 const limitSize = 50 * 1024 * 1024;  
 if (!_param.fileSize || _param.fileSize > limitSize) {  
 return resultData(-1, '音频文件大小不能超过50M!');  
 }  
 const access_token = await getAccessToken();  
 if (!access_token) {  
 return resultData(-1, 'AccessToken获取失败!');  
 }  
  
 try {  
 //文件格式  
 let fileFormat = _param.fileName.split('.')[1];  
 let obj = null;  
 await cloud.fetch({  
 url: 'https://aip.baidubce.com/rpc/2.0/aasr/v1/create?access_token=' + access_token,  
 method: 'POST',  
 headers: {  
 'Content-Type': 'application/json',  
 'Accept': 'application/json'  
 },  
 data: {  
 'speech_url': _param.fileUrl, //音频url,云端可外网访问的url链接,音频大小不超过500MB  
 'format': fileFormat, //音频格式,['mp3', 'wav', 'pcm','m4a','amr']单声道,编码 16bits 位深  
 'pid': _param && _param.pid ? _param.pid : 80001, //语言类型,[80001(中文语音近场识别模型极速版), 80006(中文音视频字幕模型,1737(英文模型)]  
 'rate': 16000 //采样率,[16000] 固定值  
 }  
 }).then(function (res) {  
 let d = res.data;  
 if (res.status == 200 && d.task_id) {  
 obj = resultData(0, '音频转写成功!', d);  
 }  
 else {  
 obj = resultData(-1, '音频转写失败!' + d.error_msg);  
 }  
 }).catch(function (err) {  
 console.log('音频转写异常!', err.message);  
 obj = resultData(-1, '音频转写异常:' + err.message);  
 });  
  
 return obj;  
 }  
 catch (e) {  
 return resultData(-1, '异常错误:' + e.message);  
 }  
}  
  
//音频转写-查询任务结果  
async function searchVoiceToText(param) {  
 console.log('searchVoiceToText', param);  
 const _param = param;  
 //参数校验  
 if (!_param.taskIds || _param.taskIds.length == 0) {  
 return resultData(-1, '参数taskIds不能为空!');  
 }  
  
 const access_token = await getAccessToken();  
 if (!access_token) {  
 return resultData(-1, 'AccessToken获取失败!');  
 }  
  
 try {  
 let obj = null;  
 //音频转写-查询任务  
 await cloud.fetch({  
 url: 'https://aip.baidubce.com/rpc/2.0/aasr/v1/query?access_token=' + access_token,  
 method: 'POST',  
 headers: {  
 'Content-Type': 'application/json',  
 'Accept': 'application/json'  
 },  
 data: {  
 'task_ids': _param.taskIds, //任务id,推荐一次查询多个任务id,单次最多可查询200个  
 }  
 }).then(function (res) {  
 let d = res.data;  
 if (res.status == 200 && d.tasks_info) {  
 obj = resultData(0, '音频转写-查询成功!', d.tasks_info);  
 }  
 else {  
 obj = resultData(-1, '音频转写-查询失败!' + d.error_msg);  
 }  
 }).catch(function (err) {  
 console.log('音频转写-查询异常!', err.message);  
 obj = resultData(-1, '音频转写-查询异常:' + err.message);  
 });  
  
 return obj;  
 }  
 catch (e) {  
 return resultData(-1, '异常错误:' + e.message);  
 }  
}  
  
// 获取AccessToken  
async function getAccessToken() {  
 let access_token = '';  
 try {  
 await cloud.fetch({  
 url: 'https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=' + apiKey + '&client_secret=' + secretKey,  
 method: 'POST',  
 headers: {  
 'Content-Type': 'application/json',  
 'Accept': 'application/json'  
 }  
 }).then(function (res) {  
 let d = res.data;  
 if (res.status == 200 && d.access_token) {  
 access_token = d.access_token;  
 }  
 else {  
 console.log('获取AccessToken失败!' + d.error_msg);  
 }  
 }).catch(function (err) {  
 console.log('获取AccessToken异常!', err.message);  
 });  
 }  
 catch (e) {  
 console.log('异常错误:' + e.message);  
 }  
 return access_token;  
}  
  
//返回结果数据  
function resultData(code = -1, msg = '', data = null) {  
 return { code, msg, data }  
}

Laf结合七牛云对象存储轻松实现文件上传

laf+uniapp三分钟轻松玩转ChatGPT

零基础使用Laf三分钟免费将ChatGPT接入微信公众号

新手小白如何注册OpenAI账号并试用ChatGPT

0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
基于火山引擎 EMR 构建企业级数据湖仓
火山引擎 EMR 是一款云原生开源大数据平台,提供主流的开源大数据引擎,加持了字节跳动内部的优化、海量数据处理的最佳实践。本次演讲将为大家介绍火山引擎 EMR 的架构及核心特性,如何基于开源架构构建企业级数据湖仓,同时向大家介绍火山 EMR 产品的未来规划。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论