一,实现思路
相同点
1, 注册账号
2,创建应用
3,获取id,key和secret
4,转换之前都需要先获取授权
不同之处
科大讯飞
1,客户端与服务端通过websock交换数据;
2,客户端通过将必要信息加密追加到wss后面进行授权;
3,转换语音后,服务器将音频文件切成多个小段,转成base64返回到客户端;
4,客户端收到信息后,将每一段信息postMessage到子进程进行转码解析操作,然后子进程再把解析后的数据返回到主进程,主进程边合成边播放,通过bufferSource进行播放,不通过audio播放;
5,子进程那一段js需要在项目中安装worker-loader加载器,并进行必要配置进行解析,否则会报错;
百度
1,客户端与服务端通过http进行数据交换;
2,获取accessToken接口,不支持跨域,需要配置nginx进行代理转发;
3,转换语音也是通过http发送,服务器转换完毕后,客户端将语音文件全部下载下来,在dom中插入audio标签,因为谷歌浏览器的限制,大部分情况不能播放,需要在提供的钩子函数中,手动调用play方法进行播放;
阿里云
1,阿里要后台RAM访问管理中建立AccessKeyId和AccessKeySecret才能进行后续API操作;
1,阿里为了安全起见,没有提供浏览器端的demo,提供了不同语言的服务端demo,庆幸的是有nodejs端的;
2,获取token需要安装阿里提供的库;
3,为了不依赖服务端,纯浏览器运行解决跨域问题,需要对阿里提供的库进行部分阉割处理,移除掉url的判断和其依赖库httpx进行修改这阉割;
3,拿到accessToken后,就可以使用阿里提供的RESTFULL风格的 API,不用看nodejs版本的了;
代码贴图
全局公共配置
config.js
var CONFIG = {
...,
ttsPlatform: 'bd', //文本转换平台,kdxf:科大讯飞,bd:百度
ttsSet: {
kdxf: {
APPID: '931ea87e', //科大讯飞文字转语音APPID,文档参见:https://www.xfyun.cn/doc/tts/online_tts/API.html
API_SECRET: 'MzA5OGMwY2UyZjMwNWRmMjVlZjg2NDBh', //科大讯飞文字转语音API_SECRET
API_KEY: 'd43f241bdb57c4d873c8a8bbf90df713', //科大讯飞文字转语音API_KEY
VCN: 'xiaoyan' //科大讯飞文字转语音音色VCN,xiaoyan,aisjiuxu,aisxping,aisjinger,aisbabyxu
},
bd: {
APPID: '26392936', //百度文字转语音APPID,文档参见:https://cloud.baidu.com/doc/SPEECH/s/Gk38y8hiw
API_SECRET: 'ewnIzMkiyoZzP4fvandpIQcEO7BMoxvQ', //百度文字转语音SecretKey
API_KEY: 'QiGzo2yq8kGqxB5HGzh9Nc6p', //百度文字转语音API_KEY
VCN: 4 //基础音库:度小宇=1,度小美=0,度逍遥(基础)=3,度丫丫=4;精品音库:度逍遥(精品)=5003,度小鹿=5118,度博文=106,度小童=110,度小萌=111,度米朵=103,度小娇=5
},
ali: {
AccessKeyId: 'LTAI5tAw3BxzatA4wNC6D3m2', //和下方一起创建地址,参见主账号创建AK密钥:https://ram.console.aliyun.com/overview
AccessKeySecret: 'pU0S8lX9lssPDWAg5EmJ4BIxnGwbZx',
Appkey: '3iRZrsrhftJYvDkP'
}
},
...
};
科大讯飞
kdxf_tts.js
// 科大讯飞TTS文件库
import CryptoJS from 'crypto-js';
// import Enc from 'enc';
import TransWorker from './transcode.worker.js';
// import VConsole from 'vconsole';
import { Base64 } from 'js-base64';
const kdxftts = ({ APPID, API_SECRET, API_KEY, VCN = 'xiaoyan' }) => {
if (APPID == '' || API_SECRET == '' || API_KEY == '') {
return console.error('请在配置文件中配置ttsSet[kdxf]必要的信息');
}
//APPID,APISecret,APIKey在控制台-我的应用-语音合成(流式版)页面获取
let transWorker = new TransWorker();
function getWebsocketUrl() {
return new Promise((resolve, reject) => {
var apiKey = API_KEY;
var apiSecret = API_SECRET;
var url = 'wss://tts-api.xfyun.cn/v2/tts';
var host = location.host;
var date = new Date().toGMTString();
var algorithm = 'hmac-sha256';
var headers = 'host date request-line';
var signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/tts HTTP/1.1`;
var signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret);
var signature = CryptoJS.enc.Base64.stringify(signatureSha);
var authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`;
var authorization = btoa(authorizationOrigin);
url = `${url}?authorization=${authorization}&date=${date}&host=${host}`;
resolve(url);
});
}
class TTSRecorder {
constructor({ speed = 50, voice = 50, pitch = 50, voiceName = VCN, appId = APPID, text = '', tte = 'UTF8', defaultText = '请输入您要合成的文本' } = {}) {
this.speed = speed;
this.voice = voice;
this.pitch = pitch;
this.voiceName = voiceName;
this.text = text;
this.tte = tte;
this.defaultText = defaultText;
this.appId = appId;
this.audioData = [];
this.rawAudioData = [];
this.audioDataOffset = 0;
this.status = 'init';
transWorker.onmessage = e => {
this.audioData.push(...e.data.data);
this.rawAudioData.push(...e.data.rawAudioData);
};
}
// 修改录音听写状态
setStatus(status) {
this.onWillStatusChange && this.onWillStatusChange(this.status, status);
this.status = status;
}
// 设置合成相关参数
setParams({ speed, voice, pitch, text, voiceName, tte }) {
speed !== undefined && (this.speed = speed);
voice !== undefined && (this.voice = voice);
pitch !== undefined && (this.pitch = pitch);
text && (this.text = text);
tte && (this.tte = tte);
voiceName && (this.voiceName = voiceName);
this.resetAudio();
}
// 连接websocket
connectWebSocket() {
this.setStatus('ttsing');
return getWebsocketUrl().then(url => {
let ttsWS;
if ('WebSocket' in window) {
ttsWS = new WebSocket(url);
} else if ('MozWebSocket' in window) {
ttsWS = new MozWebSocket(url);
} else {
alert('浏览器不支持WebSocket');
return;
}
this.ttsWS = ttsWS;
ttsWS.onopen = e => {
this.webSocketSend();
this.playTimeout = setTimeout(() => {
this.audioPlay();
}, 1000);
};
ttsWS.onmessage = e => {
this.result(e.data);
};
ttsWS.onerror = e => {
clearTimeout(this.playTimeout);
this.setStatus('errorTTS');
alert('WebSocket报错,请f12查看详情');
console.error(`详情查看:${encodeURI(url.replace('wss:', 'https:'))}`);
};
ttsWS.onclose = e => {
console.log(e);
};
});
}
// 处理音频数据
transToAudioData(audioData) {}
// websocket发送数据
webSocketSend() {
var params = {
common: {
app_id: this.appId // APPID
},
business: {
aue: 'raw',
auf: 'audio/L16;rate=16000',
vcn: this.voiceName,
speed: this.speed,
volume: this.voice,
pitch: this.pitch,
bgs: 1,
tte: this.tte,
ttp: 'cssml'
},
data: {
status: 2,
text: this.encodeText(this.text || this.defaultText, this.tte === 'unicode' ? 'base64&utf16le' : '')
}
};
this.ttsWS.send(JSON.stringify(params));
}
encodeText(text, encoding) {
switch (encoding) {
case 'utf16le': {
let buf = new ArrayBuffer(text.length * 4);
let bufView = new Uint16Array(buf);
for (let i = 0, strlen = text.length; i < strlen; i++) {
bufView[i] = text.charCodeAt(i);
}
return buf;
}
case 'buffer2Base64': {
let binary = '';
let bytes = new Uint8Array(text);
let len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
case 'base64&utf16le': {
return this.encodeText(this.encodeText(text, 'utf16le'), 'buffer2Base64');
}
default: {
return Base64.encode(text);
}
}
}
// websocket接收数据的处理
result(resultData) {
let jsonData = JSON.parse(resultData);
// 合成失败
if (jsonData.code !== 0) {
alert(`合成失败: ${jsonData.code}:${jsonData.message}`);
console.error(`${jsonData.code}:${jsonData.message}`);
this.resetAudio();
return;
}
transWorker.postMessage(jsonData.data.audio);
if (jsonData.code === 0 && jsonData.data.status === 2) {
this.ttsWS.close();
}
}
// 重置音频数据
resetAudio() {
this.audioStop();
this.setStatus('init');
this.audioDataOffset = 0;
this.audioData = [];
this.rawAudioData = [];
this.ttsWS && this.ttsWS.close();
clearTimeout(this.playTimeout);
}
// 音频初始化
audioInit() {
let AudioContext = window.AudioContext || window.webkitAudioContext;
if (AudioContext) {
this.audioContext = new AudioContext();
this.audioContext.resume();
this.audioDataOffset = 0;
}
}
// 音频播放
audioPlay() {
this.setStatus('play');
let audioData = this.audioData.slice(this.audioDataOffset);
this.audioDataOffset += audioData.length;
let audioBuffer = this.audioContext.createBuffer(1, audioData.length, 22050);
let nowBuffering = audioBuffer.getChannelData(0);
if (audioBuffer.copyToChannel) {
audioBuffer.copyToChannel(new Float32Array(audioData), 0, 0);
} else {
for (let i = 0; i < audioData.length; i++) {
nowBuffering[i] = audioData[i];
}
}
let bufferSource = (this.bufferSource = this.audioContext.createBufferSource());
bufferSource.buffer = audioBuffer;
bufferSource.connect(this.audioContext.destination);
bufferSource.start();
bufferSource.onended = event => {
if (this.status !== 'play') {
return;
}
if (this.audioDataOffset < this.audioData.length) {
this.audioPlay();
} else {
this.audioStop();
}
};
}
// 音频播放结束
audioStop() {
this.setStatus('endPlay');
clearTimeout(this.playTimeout);
this.audioDataOffset = 0;
if (this.bufferSource) {
try {
this.bufferSource.stop();
} catch (e) {
console.log(e);
}
}
}
start() {
if (this.audioData.length) {
this.audioPlay();
} else {
if (!this.audioContext) {
this.audioInit();
}
if (!this.audioContext) {
alert('该浏览器不支持webAudioApi相关接口');
return;
}
this.connectWebSocket();
}
}
stop() {
this.audioStop();
}
}
return TTSRecorder;
};
export default kdxftts;
子进程文件transcode.worker.js
(function() {
let minSampleRate = 22050;
self.onmessage = function(e) {
transcode.transToAudioData(e.data);
};
var transcode = {
transToAudioData: function(audioDataStr, fromRate = 16000, toRate = 22505) {
let outputS16 = transcode.base64ToS16(audioDataStr);
let output = transcode.transS16ToF32(outputS16);
output = transcode.transSamplingRate(output, fromRate, toRate);
output = Array.from(output);
self.postMessage({
data: output,
rawAudioData: Array.from(outputS16)
});
},
transSamplingRate: function(data, fromRate = 44100, toRate = 16000) {
var fitCount = Math.round(data.length * (toRate / fromRate));
var newData = new Float32Array(fitCount);
var springFactor = (data.length - 1) / (fitCount - 1);
newData[0] = data[0];
for (let i = 1; i < fitCount - 1; i++) {
var tmp = i * springFactor;
var before = Math.floor(tmp).toFixed();
var after = Math.ceil(tmp).toFixed();
var atPoint = tmp - before;
newData[i] = data[before] + (data[after] - data[before]) * atPoint;
}
newData[fitCount - 1] = data[data.length - 1];
return newData;
},
transS16ToF32: function(input) {
var tmpData = [];
for (let i = 0; i < input.length; i++) {
var d = input[i] < 0 ? input[i] / 0x8000 : input[i] / 0x7fff;
tmpData.push(d);
}
return new Float32Array(tmpData);
},
base64ToS16: function(base64AudioData) {
base64AudioData = atob(base64AudioData);
const outputArray = new Uint8Array(base64AudioData.length);
for (let i = 0; i < base64AudioData.length; ++i) {
outputArray[i] = base64AudioData.charCodeAt(i);
}
return new Int16Array(new DataView(outputArray.buffer).buffer);
}
};
})();
vue.config.js
npm i worker-loader -D
module.exports = {
...,
chainWebpack: config => {
config.module
.rule('worker')
.test(/\.worker\.js$/)
.use('worker')
.loader('worker-loader')
.options({
inline: 'fallback'
})
.end();
}
}
百度
参考科大讯飞代码封装bd_tts.js
import btts from './baidu_tts_cors';
import dayjs from 'dayjs';
const bdtts = ({ APPID, API_KEY, API_SECRET, VCN }) => {
if (APPID == '' || API_SECRET == '' || API_KEY == '') {
return console.error('请在配置文件中配置ttsSet[bd]必要的信息');
}
class TTSRecorder {
constructor({ appId = APPID, apiKey = API_KEY, apiSecret = API_SECRET, vcn = VCN } = {}) {
this.appId = appId;
this.apiKey = apiKey;
this.apiSecret = apiSecret;
this.cuid = 'WEB'; //用户唯一标识,用来计算UV值。建议填写能区分用户的机器 MAC 地址或 IMEI 码,长度为60字符以内
this.ctp = 1; //客户端类型选择,web端填写固定值1
this.lan = 'zh'; //固定值zh。语言选择,目前只有中英文混合模式,填写固定值zh
this.spd = 5; //语速,取值0-15,默认为5中语速
this.pit = 5; //音调,取值0-15,默认为5中语调
this.vol = 15; //音量,取值0-15,默认为5中音量(取值为0时为音量最小值,并非为无声)
this.aue = 3; //3为mp3格式(默认); 4为pcm-16k;5为pcm-8k;6为wav(内容同pcm-16k); 注意aue=4或者6是语音识别要求的格式,但是音频内容不是语音识别要求的自然人发音,所以识别效果会受影响。
this.per = vcn; ////基础音库:度小宇=1,度小美=0,度逍遥(基础)=3,度丫丫=4;精品音库:度逍遥(精品)=5003,度小鹿=5118,度博文=106,度小童=110,度小萌=111,度米朵=103,度小娇=5
this.token = ''; //先初始化token
this.expires_in = 0; //token过期时间
}
getToken() {
return new Promise((resolve, reject) => {
fetch(`/bdtts/oauth/2.0/token?grant_type=client_credentials&client_id=${this.apiKey}&client_secret=${this.apiSecret}`, {
method: 'post'
})
.then(res => {
if (res.status === 200) {
return res.json();
} else {
return Promise.reject(res.json());
}
})
.then(res => {
this.token = res.access_token;
this.expires_in = dayjs().add(res.expires_in, 'second'); //过期时间
})
.catch(() => {
reject();
});
});
}
async transTextToPlay(param = {}, options = {}) {
//没有token,或者token距离过期小于60秒,重新请求token
if (this.token == '' || dayjs(this.expires_in).diff(dayjs(), 'second') < 60) {
await this.getToken();
}
let _param = Object.assign(
{
tex: '',
tok: this.token,
cuid: this.cuid,
ctp: this.ctp,
lan: this.lan,
spd: this.spd,
pit: this.pit,
vol: this.vol,
per: this.per,
aue: this.aue
},
param
);
let _options = Object.assign(
{
volume: 1,
autoDestory: true,
// timeout: 10000,
hidden: true,
onInit: function(htmlAudioElement) {},
onSuccess: function(htmlAudioElement) {
htmlAudioElement.play();
},
onError: function(errorText) {},
onTimeout: function() {}
},
options
);
btts(_param, _options);
}
}
return TTSRecorder;
};
export default bdtts;
baidu_tts_cors.js
/**
* 浏览器调用语音合成接口
* @param {Object} param 百度语音合成接口参数
* 请参考 https://ai.baidu.com/docs#/TTS-API/41ac79a6
* @param {Object} options 跨域调用api参数
* timeout {number} 超时时间 默认不设置为60秒
* volume {number} audio控件音量,范围 0-1
* hidden {boolean} 是否隐藏audio控件
* autoDestory {boolean} 播放音频完毕后是否自动删除控件
* onInit {Function} 创建完audio控件后调用
* onSuccess {Function} 远程语音合成完成,并且返回音频文件后调用
* onError {Function} 远程语音合成完成,并且返回错误字符串后调用
* onTimeout {Function} 超时后调用,默认超时时间为60秒
*/
function btts(param, options) {
var url = 'http://tsn.baidu.com/text2audio';
var opt = options || {};
var p = param || {};
// 如果浏览器支持,可以设置autoplay,但是不能兼容所有浏览器
var audio = document.createElement('audio');
if (opt.autoplay) {
audio.setAttribute('autoplay', 'autoplay');
}
// 隐藏控制栏
if (!opt.hidden) {
audio.setAttribute('controls', 'controls');
} else {
audio.style.display = 'none';
}
// 设置音量
if (typeof opt.volume !== 'undefined') {
audio.volume = opt.volume;
}
// 调用onInit回调
isFunction(opt.onInit) && opt.onInit(audio);
// 默认超时时间60秒
var DEFAULT_TIMEOUT = 60000;
var timeout = opt.timeout || DEFAULT_TIMEOUT;
// 创建XMLHttpRequest对象
var xhr = new XMLHttpRequest();
xhr.open('POST', url);
// 创建form参数
var data = {};
for (var j in param) {
data[j] = param[j];
}
// 赋值预定义参数
data.cuid = data.cuid || data.tok;
data.ctp = 1;
data.lan = data.lan || 'zh';
data.aue = data.aue || 3;
// 序列化参数列表
var fd = [];
for (var k in data) {
fd.push(k + '=' + encodeURIComponent(data[k]));
}
// 用来处理blob数据
var frd = new FileReader();
xhr.responseType = 'blob';
xhr.send(fd.join('&'));
// 用timeout可以更兼容的处理兼容超时
var timer = setTimeout(function() {
xhr.abort();
isFunction(opt.onTimeout) && opt.onTimeout();
}, timeout);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
clearTimeout(timer);
if (xhr.status == 200) {
if (xhr.response.type === 'audio/mp3') {
// 在body元素下apppend音频控件
document.body.appendChild(audio);
audio.setAttribute('src', URL.createObjectURL(xhr.response));
// autoDestory设置则播放完后移除audio的dom对象
if (opt.autoDestory) {
audio.onended = function() {
document.body.removeChild(audio);
};
}
isFunction(opt.onSuccess) && opt.onSuccess(audio);
}
// 用来处理错误
if (xhr.response.type === 'application/json') {
frd.onload = function() {
var text = frd.result;
isFunction(opt.onError) && opt.onError(text);
};
frd.readAsText(xhr.response);
}
}
}
};
// 判断是否是函数
function isFunction(obj) {
if (Object.prototype.toString.call(obj) === '[object Function]') {
return true;
}
return false;
}
}
export default btts;
阿里云
修改阿里底层包node_modules@alicloud\pop-core\lib\rpc.js
version: 1.7.11
注释掉底层包对于url的限制
class RPCClient {
constructor(config, verbose) {
assert(config, 'must pass "config"');
assert(config.endpoint, 'must pass "config.endpoint"');
// if (!config.endpoint.startsWith('https://') &&
// !config.endpoint.startsWith('http://')) {
// throw new Error(`"config.endpoint" must starts with 'https://' or 'http://'.`);
// }
assert(config.apiVersion, 'must pass "config.apiVersion"');
assert(config.accessKeyId, 'must pass "config.accessKeyId"');
var accessKeySecret = config.secretAccessKey || config.accessKeySecret;
assert(accessKeySecret, 'must pass "config.accessKeySecret"');
2,修改阿里底层包依赖库node_modules\httpx\lib\index.js
version: ^2.1.2
1> 注释掉对url中关于localhost默认值的添加
var options = {
// host: parsed.hostname || 'localhost', //修改前
host: parsed.hostname || '',//修改后
path: parsed.path || '/',
method: method,
2> 由于nodejs库在浏览器运行,缺少一些对象,所以这里禁用掉相关的超时判断
return new Promise((resolve, reject) => {
// node.js 14 use response.client
const socket = response.socket || response.client;
const makeReadTimeoutError = () => {
const req = response.req;
var err = new Error();
err.name = 'RequestTimeoutError';
err.message = `ReadTimeout: ${socket[READ_TIME_OUT]}. ${req.method} ${req.path} failed.`;
return err;
};
// check read-timer
let readTimer;
// const oldReadTimer = socket[READ_TIMER];
// if (!oldReadTimer) {
// reject(makeReadTimeoutError());
// return;
// }
// const remainTime = socket[READ_TIME_OUT] - (Date.now() - socket[READ_TIMER_START_AT]);
// clearTimeout(oldReadTimer);
// if (remainTime <= 0) {
// reject(makeReadTimeoutError());
// return;
// }
// readTimer = setTimeout(function () {
// reject(makeReadTimeoutError());
// }, remainTime);
// start reading data
var onError, onData, onEnd;
var cleanup = function () {
// cleanup
readable.removeListener('error', onError);
readable.removeListener('data', onData);
readable.removeListener('end', onEnd);
// clear read timer
if (readTimer) {
clearTimeout(readTimer);
}
};
3,前端调用阿里库的时候,传入自定义头
自定义库 ali_tts.js
import dayjs from 'dayjs';
var RPCClient = require('@alicloud/pop-core').RPCClient;
const alitts = ({ AccessKeyId, AccessKeySecret, Appkey }) => {
if (AccessKeyId == '' || AccessKeySecret == '' || Appkey == '') {
return console.error('请在配置文件中配置ttsSet[ali]必要的信息');
}
var client = new RPCClient({
accessKeyId: AccessKeyId,
accessKeySecret: AccessKeySecret,
endpoint: '/alittsmeta', //原来必须传入http://nls-meta.cn-shanghai.aliyuncs.com
apiVersion: '2019-02-28'
});
class TTSRecorder {
constructor({ appkey = Appkey } = {}) {
this.appkey = appkey;
this.token = ''; //先初始化token
this.expires_in = 0; //token过期时间
}
getToken() {
return new Promise((resolve, reject) => {
// => returns Promise
// => request(Action, params, options)
client
.request('CreateToken')
.then(res => {
this.token = res.Token.Id;
this.expires_in = dayjs().add(res.Token.ExpireTime, 'second'); //过期时间
resolve(res);
})
.catch(err => {
reject(err);
});
});
}
async transTextToPlay(param = { text: '' }) {
//没有token,或者token距离过期小于60秒,重新请求token
if (this.token == '' || dayjs(this.expires_in).diff(dayjs(), 'second') < 60) {
await this.getToken();
}
//各位乘客,M371线路 粤B12345 即将在3分钟后发车,要上车的乘客请做好准备。
fetch(`/alittsgateway/stream/v1/tts?appkey=${this.appkey}&token=${this.token}&text=${param.text}&format=mp3&sample_rate=16000`)
.then(res => {
return res.blob();
})
.then(blob => {
var audio = document.createElement('audio');
audio.setAttribute('autoplay', 'autoplay');
audio.style.display = 'none';
audio.volume = 1;
document.body.appendChild(audio);
audio.onended = function() {
document.body.removeChild(audio);
};
audio.setAttribute('src', URL.createObjectURL(blob));
audio.play();
});
}
}
return TTSRecorder;
};
export default alitts;
4,webpack配置代理
proxy: {
'/alittsmeta': {
target: 'http://nls-meta.cn-shanghai.aliyuncs.com', //阿里token
pathRewrite: {
'/alittsmeta': ''
}
},
'/alittsgateway': {
target: 'https://nls-gateway-cn-shanghai.aliyuncs.com', //阿里tts
pathRewrite: {
'/alittsgateway': ''
}
},
vue文件调用
<template>
<div>代码示例DEMO,只列出核心调用部分</div>
</template>
<script>
import kdxftts from '../../library/kdxf_tts';
import bdtts from '../../library/bd_tts';
export default {
name: '',
components: {},
props: {},
data() {
return {};
},
computed: {},
created() {
//如果配置了文字转语音平台,则初始化
if (this.$config.ttsPlatform && this.$config.ttsSet[this.$config.ttsPlatform]) {
if (this.$config.ttsPlatform == 'kdxf') {
const TTSRecorder = kdxftts(this.$config.ttsSet[this.$config.ttsPlatform]);
this.ttsControl = new TTSRecorder();
} else if (this.$config.ttsPlatform == 'bd') {
const TTSRecorder = bdtts(this.$config.ttsSet[this.$config.ttsPlatform]);
this.ttsControl = new TTSRecorder();
} else if (this.$config.ttsPlatform == 'ali') {
const TTSRecorder = alitts(this.$config.ttsSet[this.$config.ttsPlatform]);
this.ttsControl = new TTSRecorder();
}
}
setInterval(() => {
this.currentTimestamp = Date.now();
const ms = this.$dayjs(this.currentTimestamp).format('ss');
//每分钟刷新一次
if (ms == '00') {
this.init();
}
}, 1000);
this.init();
},
mounted() {},
destroyed() {},
methods: {
async init() {
//初始化数据
const response = await this.$http.post(`/terminal.control/ScreenNoInterAction/getLineSite.do?screenNo=${this.$route.params.sn}`);
this.lines = JSON.parse(response.parameters.lines || '[]');
const weather = JSON.parse(response.parameters.weather_daily || '{}');
this.weather.code_day = weather.code_day;
this.weather.text_day = weather.text_day;
this.weather.low = weather.low;
this.weather.high = weather.high;
this.notices = JSON.parse(response.parameters.notices || '[]');
//判断是否需要播报发车
//当车辆发车时,通过引导屏自动播报发车线路及发车时间点信息,播报频率写的是发车前5分钟和1分钟。
//播报内容:各位乘客,{M371}线路{粤B12345}即将在3分钟后发车,要上车的乘客请做好准备。
this.broadcastTexts = ''; //重置语音播报信息
this.lines.forEach(line => {
let diff = this.$dayjs(line.nextTime).diff(this.$dayjs(this.currentTimestamp), 'minutes') + 1;
if (diff == 3 || diff == 1) {
//【官方】合成多音字、静音停顿、数字读法:http://bbs.xfyun.cn/thread/15340
if (this.$config.ttsPlatform == 'kdxf') {
let stationName = line.name.replace(/(\d+)/g, '<sayas type="number:digits">$1</sayas>');
let text = `<break time="1000ms"/>各位乘客,${stationName}线路,${line.nextBusLicense},即将在${diff}分钟后发车,要上车的乘客请做好准备。`;
this.broadcastTexts += text;
} else if (this.$config.ttsPlatform == 'bd' || this.$config.ttsPlatform == 'ali') {
let stationName = line.name;
let text = `各位乘客,${stationName}线路,${line.nextBusLicense},即将在${diff}分钟后发车,要上车的乘客请做好准备。`;
this.broadcastTexts += text;
}
}
});
console.log('播报文字:', this.broadcastTexts);
if (this.broadcastTexts != '') {
//判断播放平台
if (this.$config.ttsPlatform == 'kdxf') {
//科大讯飞
this.ttsControl.setParams({
text: this.broadcastTexts
});
this.ttsControl.start();
} else if (this.$config.ttsPlatform == 'bd') {
//百度
this.ttsControl.transTextToPlay({
tex: this.broadcastTexts
});
} else if (this.$config.ttsPlatform == 'ali') {
//阿里
this.ttsControl.transTextToPlay({
text: this.broadcastTexts
});
}
}
}
}
};
</script>
<style lang="scss" scoped></style>
<style scoped></style>
本地webpack代理配置
我在调用百度授权接口时,在前面加了/bdtts前缀,用以区分大屏的其他服务,转发时,将前缀删除,阿里同理
proxy: {
'/bdtts': {
target: 'https://openapi.baidu.com', //百度
pathRewrite: {
'^/bdtts': ''
}
},
'/alittsmeta': {
target: 'http://nls-meta.cn-shanghai.aliyuncs.com', //阿里token
pathRewrite: {
'/alittsmeta': ''
}
},
'/alittsgateway': {
target: 'https://nls-gateway-cn-shanghai.aliyuncs.com', //阿里tts
pathRewrite: {
'/alittsgateway': ''
}
},
...
}
nginx代理配置
location /bdtts {
proxy_pass https://openapi.baidu.com;
rewrite /bdtts(.*) $1 break;
}
location /alittsmeta {
proxy_pass http://nls-meta.cn-shanghai.aliyuncs.com;
rewrite /alittsmeta(.*) $1 break;
}
location /alittsgateway {
proxy_pass https://nls-gateway-cn-shanghai.aliyuncs.com;
rewrite /alittsgateway(.*) $1 break;
}