laf 指标
本文将用 Laf 实现以下性能:
- 短文本转音频
- 长文本转音频
- 音频转文本
laf 筹备工作
注册百度智能云
进入百度智能云官网进行注册,地址:https://cloud.baidu.com/
登陆进去之后找到右上角的产品服务 –> 语音技术。
首先支付一下收费的资源。
❝
点进去会提醒须要认证,间接集体认证一下就能够了。
把这里的 语音辨认 和语音合成 都支付了。
支付完之后咱们到利用列表中来,创立一个利用。
❝
这里只须要输出利用名称和利用面形容即可,其余的选项不必动。
创立实现后咱们失去了一个 API Key 和 Secret Key , 咱们待会要用到。
编写 Laf 云函数
咱们须要在 Laf 中创立三个云函数,别离写入以下代码。
函数一 baidu-api
❝
这里批改第四行和第五行代码,改成你刚刚创立的利用的 API Key 和 Secret Key。
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}
}
函数二 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}
}
函数三 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}
}
创立一个 bucket
在 Laf 的存储中创立一个名为 store-file 的 bucket 权限给公共读写。
laf 开始应用
短文本转音频
❝
短文本要求文本长度小于 60,大于 60 的用长文本的办法
短文本转音频比较简单,只须要调用 baidu-api 函数就能够取得音频的 URL(这里的音频文件是存储在刚创立的 bucket 中)。
调用示例
在云函数 baidu-api 右侧的调试面板抉择 POST 申请办法,body 中传入以下参数后点击运行。
{
"type": "shortTextToVoice",
"param": {"text": "明天五月初五端午节,祝大家端午节健康!"}
}
OK 这里咱们就取得了短文本转音频后的音频文件的 URL。
长文本转音频
因为长文本和短文本不一样,当内容过多之后不能实时的返回音频文件,故须要先创立工作,而后再通过工作 ID 去查问生成的音频文件。
调用示例
在云函数 baidu-api 右侧的调试面板抉择 POST 申请办法,body 中传入以下参数后点击运行。
❝
这里 text 是一个数组,外面能够放一个很长的字符串,也能够放多个字符串,区别是多个字符串两头朗诵会有进展。字符总长度不能超过十万。
{
"type": "longTextToVoice",
"param": {"text": ["明天五月初五端午节","祝大家端午节健康!"]
}
}
这样咱们就创立了一个工作,并且失去了这个工作的 ID。
而后咱们依据这个工作的 ID 来查问转换之后的音频文件,持续调用此函数传入以下参数。
❝
这里的 taskIds 换成你刚刚失去的工作 ID
{
"type": "searchTextToVoice",
"param": {"taskIds": ["649804c2d9ab330001cf1ea6"]
}
}
调用后咱们会失去这个工作目前的状态和音频文件的 URL。
音频文件转文本
想要把音频文件转成文本,首先须要这个文件的 URL,你能够手动上传到 Laf 的存储 bucket 中,也能够这样调用咱们创立的 upload-file 云函数来上传文件并获取到 URL。
总之咱们当初有了一个音频文件的 URL 想要转成文字须要持续调用咱们的 baidu-api 云函数。
调用示例
在云函数 baidu-api 右侧的调试面板抉择 POST 申请办法,body 中传入以下参数后点击运行。
❝
这里的 fileUrl 改成你须要转文字的音频 URL
{
"type": "createVoiceToText",
"param": {
"fileUrl": "https://cofxat-store-file.oss.laf.run/TextToVoice/344762599164.mp3",
"fileName": "TempFiles/96bcdd36-373a-4031-b843-33d45c17dc03.mp3",
"fileType": "audio/mpeg",
"fileSize": 1001504
}
}
同样的咱们运行之后会失去一个工作 ID 咱们须要依据这个工作的 ID 来查问转换之后的后果。
音频文件转文本查问,调用云函数 baidu-api 传入以下参数。
❝
这里的 taskIds 换成你刚刚失去的工作 ID
{
"type": "searchVoiceToText",
"param": {"taskIds": ["6498082dd9ab330001cf1f9f"]
}
}
调用之后胜利失去文本。
Ok!至此咱们用 Laf 实现了文本和音频的自在转换!
对于 Laf
Laf 是一款为所有开发者打造的集函数、数据库、存储为一体的云开发平台,助你像写博客一样写代码,随时随地公布上线利用!3 分钟上线 ChatGPT 利用!
🌟GitHub:https://github.com/labring/laf
🏠官网(国内):https://laf.run
🌎官网(海内):https://laf.dev
💻开发者论坛:https://forum.laf.run
sealos 以 kubernetes 为内核的云操作系统发行版,让云原生简略遍及
laf 写代码像写博客一样简略,什么 docker kubernetes 通通不关怀,我只关怀写业务!