很多时候在看文章的时候都会有自动朗读文章内容的功能,那么这种功能如何在 h5 上是怎么实现的呢,下面就拿我司一个基本需求作为线索,看是怎么一步一步实现的
需求提出
经过我司产品经理的想法,做出如下功能 1. 自动朗读当前 h5 页面文章
竞品——》
调研发现,竞品 h5 是 app 原生实现,而我司都是 h5 实现文章阅读,所以开始进行 h5 的调研
对接科大讯飞在线语音合成
调研发现科大讯飞的在线语音合成可以基本提供相应功能,决定做一个 demo 来测试效果
1. 控制台开通权限
2. 阅读文档
具体代码如下
import axios from ‘axios’
import * as md5 from ‘./md5’
axios.defaults.withCredentials = true
let Appid = ‘xxxxx’
let apiKey = ‘xxxxxx’
let CurTime = Date.parse(new Date()) / 1000
let param = {
auf: ‘audio/L16;rate=16000’,
aue: ‘lame’,
voice_name: ‘xiaoyan’,
speed: ’50’,
volume: ’50’,
pitch: ’50’,
engine_type: ‘intp65’,
text_type: ‘text’
}
let Base64 = {
encode: (str) => {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
function toSolidBytes(match, p1) {
return String.fromCharCode(‘0x’ + p1);
}));
},
decode: (str) => {
// Going backwards: from bytestream, to percent-encoding, to original string.
return decodeURIComponent(atob(str).split(”).map(function (c) {
return ‘%’ + (’00’ + c.charCodeAt(0).toString(16)).slice(-2);
}).join(”));
}
}
let xp = Base64.encode(JSON.stringify(param))
let CheckSum = md5.hex_md5(apiKey + CurTime + xp)
let headers = {
‘X-Appid’: Appid,
‘X-CurTime’: CurTime,
‘X-Param’: xp,
‘X-CheckSum’: CheckSum,
‘Content-Type’: ‘application/x-www-form-urlencoded; charset=utf-8’
}
export function getAloud (text) {
// let data = {
// text: encodeURI(text)
// }
var formdata = new FormData()
formdata.append(‘text’, text)
return axios({
baseURL: window.location.href.includes(‘demo’) ? ‘https://api.xfyun.cn’ : ‘/tts’,
method: ‘POST’,
url: ‘/v1/service/v1/tts’,
headers: {
…headers
},
data: formdata
})
}
经过测试,是返回二进制文件流了但是前端试了各种办法没有实现流的播放
node 中间层
引入 node 中间层是考虑到文件可以存储,可以放到 cdn 上进行缓存,可以减少相似文章的请求科大讯飞接口,可以减少流量的产生,所以决定加入 node 中间层
ps:考拉阅读有 node 服务器作为一些中间层的处理。主要技术栈是 node + koa2 + pm2
const md5 = require(‘../lib/md5.js’)
const fs = require(‘fs’)
const path = require(‘path’)
const marked = require(‘marked’)
const request = require(‘request’)
let Appid = ”
let apiKey = ”
let CurTime
let param = {
auf: ‘audio/L16;rate=16000’,
aue: ‘lame’,
voice_name: ‘x_yiping’,
speed: ’40’,
volume: ’50’,
pitch: ’50’,
engine_type: ‘intp65’,
text_type: ‘text’
}
var b = new Buffer(JSON.stringify(param));
let xp = b.toString(‘base64’)
let CheckSum
let headers
exports.getAloud = async ctx => {
CurTime = Date.parse(new Date()) / 1000
CheckSum = md5.hex_md5(apiKey + CurTime + xp)
headers = {
‘X-Appid’: Appid,
‘X-CurTime’: CurTime,
‘X-Param’: xp,
‘X-CheckSum’: CheckSum,
‘Content-Type’: ‘application/x-www-form-urlencoded; charset=utf-8’
}
let id = ctx.request.body.id
let text = ctx.request.body.text
console.log(ctx.query)
var postData = {
text: text
}
let r = request({
url: ‘http://api.xfyun.cn/v1/service/v1/tts’, // 请求的 URL
method: ‘POST’, // 请求方法
headers: headers,
formData: postData
}, function (error, response, body) {
// console.log(‘error:’, error); // Print the error if one occurred
// console.log(‘statusCode:’, response && response.statusCode); // Print the response status code if a response was received
// console.log(‘body:’, body); // Print the HTML for the Google homepage.
})
await new Promise((resolve, reject) => {
let filePath = path.join(__dirname, ‘public/’) + `/${id}.mp3`
const upStream = fs.createWriteStream(filePath)
r.pipe(upStream)
upStream.on(‘close’, () => {
console.log(‘download finished’);
resolve()
});
})
.then((res) => {
ctx.body = {
code: 200,
message: ‘ 语音合成成功 ’,
data: {
url: ‘https://fe.koalareading.com/file/’ + id + ‘.mp3’
}
}
})
}
主要运用 request 的管道流概念把后台返回的二进制文件导入到流里面,在写入到文件里面最后返回一个 url 给前端播放使用
此致,测试
// 返回 url。相同文章唯一 id 区分,可以缓存使用
需求 demo 完成