大家都在卷原理,让我们一起写业务,哈哈哈哈
需求描述
语音播报题目和选项信息,答题后播报下一题的内容,答题播放本次答题结果和建议。
需求分析
- 题目和选项信息固定,约有100道题
- 答题结果评分结果固定枚举分数情况和对应等级,约100种
- 最后的建议为AI实时生成
技术方案
- 方案1:利用语音生成网站生成固定的文案MP3,建议由后端返回语音文件
- 优势:简单,没难度
- 劣势:重复劳动,建议展示无AI效果,接口时间长
- 方案2:利用科大讯飞长文本语音api文档生成固定文案MP3,“建议”用实时语音生成api生成
- 优势:无重复工作,“建议”呈现效果佳
- 劣势:需要对接科大讯飞,实时生成的api涉及费用
最终方案:api生成固定文案,“AI建议”做个demo演示先😂(人间真实)
具体实现
固定文字生成
对接流程
【敲黑板】根据文档,流程大致是这样的,先用/v1/private/dts_create接口生成语音创建任务,接口会返回一个taskId,然后你可以调用/private/dts_query来查询用taskId查询结果,会返回生成状态,如果完成接口内会带有mp3加密的base64地址。
主要难点
调用接口需要许可,所以主要的难点就是按照文档给的规则生成带着鉴权信息的调用请求
代码实现
鉴权实现
- 获取签名
function getSignature(host, date, requestLine, apiSecret) {
const signatureOrigin = `host: ${host}\ndate: ${date}\n${requestLine}`;
const hmac = crypto.createHmac('sha256', apiSecret);
hmac.update(signatureOrigin);
return hmac.digest('base64');
}
- 根据签名生成authorization
function getAuthorization(apiKey, signature) {
const authorizationOrigin =
`api_key="${apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signature}"`;
return Buffer.from(authorizationOrigin).toString('base64');
}
【敲重点】这里文档有个错误
- 获取带鉴权的请求url
const axios = require('axios');
const crypto = require('crypto');
const fs = require('fs');
const APPID = 'testAPPID'; // replace with your APPID
const APIKey = 'testAPIKey'; // replace with your APIKey
const APISecret = 'testAPISecret'; // replace with your APISecret
const getUrl = (type) => {
const host = 'api-dx.xf-yun.com';
const date = new Date().toUTCString();
const requestLine = `POST /v1/private/${type} HTTP/1.1`;
const signature = getSignature(host, date, requestLine, APISecret);
const authorization = getAuthorization(APIKey, signature);
return `https://api-dx.xf-yun.com/v1/private/${type}?host=${host}&date=${encodeURIComponent(date)}&authorization=${encodeURIComponent(authorization)}`;
}
生成任务
async function createTask(text, fileName) {
// 请求数据
const requestData = {
header: {
app_id: APPID
},
parameter: {
dts: {
vcn: "x4_yeting",
language: "zh",
speed: 50,
volume: 50,
pitch: 50,
rhy: 1,
audio: {
encoding: "lame",
sample_rate: 16000
},
pybuf: {
encoding: "utf8",
compress: "raw",
format: "plain"
}
}
},
payload: {
text: {
encoding: "utf8",
compress: "raw",
format: "plain",
text: Buffer.from(text).toString('base64')
}
}
};
// 请求头
const headers = {
'Content-Type': 'application/json',
};
try {
const response = await axios.post(getUrl('dts_create'), requestData, { headers });
if (response.data) {
tasks[response.data.header.task_id] = fileName;
console.log('task_id',response.data.header.task_id)
}
} catch (error) {
console.error('Error:', error.response);
}
}
查询任务并保存MP3至本地
const getTaskAudio = async (taskId, outPath) => {
const headers = {
'Content-Type': 'application/json',
};
const resp = await axios.post(getUrl('dts_query'), {
"header": {
"app_id": APPID,
"task_id": taskId
}
}, {headers})
// 提取出音频数据的Base64编码字符串
const audioDataB64 = resp.data.payload.audio.audio;
// 使用Buffer将Base64字符串转换为Buffer对象
const audioUrl = Buffer.from(audioDataB64, 'base64');
const response = await axios({
method: 'GET',
url: audioUrl,
responseType: 'stream'
})
// 创建一个可写流到文件
const writer = fs.createWriteStream(`./scale/mp3/${outPath}.mp3`);
// 管道下载的流到文件
response.data.pipe(writer);
// 监听'finish'事件,当写入完成时触发
writer.on('finish', () => {
console.log(`${outPath}: saved!`);
});
}
因为最后得到MP3链接,在业务上我们不能将这样重要的静态资源外置,所以下载至本地,再上传到oss是比较合理的。
最后写个循环调用就OK了
话说请问一下这代码贴过来为啥格式全没了?