完整的记录一次大屏幕 语音播报的需求解决

背景

给客户的车间做了一个呼料的系统,大屏幕可以实时显示产线的呼料需求(APP),一方面仓库人员可以手机上看到这些呼料需求,另一方面,仓库的大屏幕可以显示分配到此仓库的呼料需求.

新需求

客户希望有新的呼料请求的时候,大屏幕不仅仅要显示,而且要语音提醒.
因为我们大屏幕用的是 Chrome 的浏览器来显示呼料请求,所以直接使用Chrome 的 TTS 接口即可:

function speakOut(msg) {
    var u = new SpeechSynthesisUtterance();
    u.lang = 'zh-CN';
    u.text = msg;
    window.speechSynthesis.speak(u);
};

然后其他的逻辑就不写了,比如如何判断新到语音,如何不再播报已经播报过的,如何播报捡料超时的任务等等.

新问题

问题来自我们的大屏幕客户端,原来的硬件结构是 电视机+小主机(插音箱)+Chrome .
现在换成了 树莓派 RespberryPi(插音箱)+电视机+Chromium(Electron 写了个壳子用来保证无人值守)
问题就变成了一连串:

  1. Chromium在树莓派上貌似没有 TTS 的发音,Google 了一堆,又是加启动参数,又是要装 eSpeak 之类的.全试过没用.具体的表现就是:window.speechSynthesis.getVoices()返回的数组是[].没有Voice,当然读不出来.
  2. 因为大屏幕终端很多,就算这种方法可行,也不可能一个一个树莓派去调整
  3. 所以更换思路,准备换成 Web 版本的接口,利用 js 来远程调用播放 MP3
  4. 找了很多 Web 的接口,貌似都收费...
  5. 终于,下定决心 自己搭一个TTS语音服务器

首先感谢 http://www.eguidedog.net/cn/ekho.php 的 余音(Ekho) 项目,非常给力.
在服务器上装了Linux发行版.具体的安装教程在:
http://www.eguidedog.net/doc/doc_install_ekho.php 下方 ++CENTOS6安装Ekho的方法++

打算帮他搞一个 Docker 版本的方便自己和大家.放到我的 TODOList 里
具体的安装的时候,还有些小问题,貌似 scl 我没装上, 不过没影响到后续使用.
安装的位置在 : /root/eGuideDog/ekho-8.0
装好之后用 Python 写了个脚本,基于 flask 的,非常简单:
假设以后提供服务的 tts 服务器地址为 tts.todd.com

from flask import Flask, request, jsonify
import uuid
import os
import sys

@app.route('/speak')
def speak():
    txt = request.args.get('txt', '').encode('utf-8').strip()
    if not txt:
        ret = {
            "msg": 'txt is required!',
            "code": 0,
        }
        return jsonify(ret)

    file_name = str(uuid.uuid1()) + ".mp3"
    path = "/data/wwwroot/tts.todd.com/" + file_name
    http_path = "http://tts.todd.com/" + file_name
    # TODO  监控文件过多后自动清除历史文件

    os.environ['PATH'] += os.pathsep + '/root/eGuideDog/ekho-8.0'
    os.environ['EKHO_DATA_PATH'] = '/root/eGuideDog/ekho-8.0/ekho-data/'

    cmd = "ekho -o %s  -t=mp3  '%s'" % (path, txt)
    # 其他需要啥参数,自己来
    os.system(cmd)

    ret = {
        "cmd": cmd,
        "path": http_path,
        "code": 1,
    }
    return jsonify(ret)


if __name__ == '__main__':
    app.run()

注意里面的几个坑:

  • 文件太多没有自动清理
  • 没有鉴权,不要放出来给别人,容易文件爆炸
  • 因为我装ekho的时候,有些路径问题,所以我启动 cmd 之前,加了环境变量.
  • nginx 要在前面反向代理:
location ~ .*\.mp3{
    expires 1s;
    access_log off;
  }
  location / {
           add_header Access-Control-Allow-Origin *;
           add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
           add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
            proxy_pass   http://127.0.0.1:5000/;
            proxy_redirect  off;
            proxy_set_header  Host $host;
            proxy_set_header X-Forwarded-Proto https;
            proxy_set_header  X-Real-IP $remote_addr;
            proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header   Cookie $http_cookie;
            proxy_connect_timeout 600s;
            proxy_send_timeout 600s;
            proxy_read_timeout 600s;
            chunked_transfer_encoding       off;
  }

上面大部分不用设置,前三行为了防止 js 调用的时候跨域.第四行用来转给 flask.
其他都不重要.

截至目前 tts 服务器好了.
接下来是前端调用:
把原来的 speakOut函数做一个改造:

function speakOut(msg) {
    if (window.speechSynthesis.getVoices().length > 0) {
        var u = new SpeechSynthesisUtterance();
        u.lang = 'zh-CN';
        u.text = msg;
        window.speechSynthesis.speak(u);
    } else {
        $.get("http://tts.todd.com/speak?txt=" + encodeURIComponent(msg), function (res) {
            if (res) {
                var audio = $("