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 Keyconst 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);  }}// 获取AccessTokenasync 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 利用 appidconst 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;}//获取文件 urlasync 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-fileimport 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通通不关怀,我只关怀写业务!