记录VUE大屏项目集成科大讯飞TTS、百度TTS和阿里TTS

1,200 阅读6分钟

一,实现思路

相同点

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;
}