小白一个防止TTS语音播报冲突的流程

这是借着前面《 小白超详细解读: 家庭留言-基于企业微信和小度TTS播报 》之后,我快速的把能够播报的很多消息统统塞进了语音播报。顿时感觉技术不再冰冷,家人看我的眼神也略略发生了变化。但是,麻烦还是来了,播报的声音经常被另一个声音打断,技术开始让家里变得噪杂了。原来语音播报不是这么简单的啊。改善改善,接下来就会有一堆的改善题目要做。下面是一个迫切的改善流程,让播报的语音一条一条出来,禁止插队,严防噪音。
经过一段时间的使用,效果还不错,就在这里分享给大家。

背景和目的

我的TTS语音播报,使用的hass下比较简单的dlna音响播放器。这个播放器存在一个比较大的问题是,当前一个语音播报尚未完成时,如果来了一条新播报消息,则新消息会被立即播报,而前一条消息就此消失。我现在把很多的消息都引入到TTS语音播报中,播报的消息经常被截掉,这快让我崩溃了。我必须尽快引入一个控制流程,在当前语音消息未播放完成,后续的所有消息进入队列排队,然后再一条一条播放出来。

另外,在编写这个控制流程过程中发现,我无法获得小度play的当前状态,无法判断其是正在播音,还是闲置。这样我又引入了一个机制,间接计算播放器可能的状态,并由这个状态去控制是否播放下一条消息。在下面的说明中,我给它起名字叫“正常队列控制流程”和“第一条消息控制流程”。

关键node节点

  1. q-gate:是需要安装的节点。队列节点,用于消息排队,并可控制队列中的消息进行释放
  2. delay:是缺省安装的节点。用于队列释放消息的延时,可通过输入时间,更改delay时间。这样就可以为队列中每一条消息定制其释放的时间点
  3. status:是缺省安装的节点。用于查询和输出q-gate节点,和delay节点的状态,以便对队列的消息释放进行控制

流程说明

  1. 每一条消息在进行播报之前,首先进入队列节点进行排队
  2. 如果是第一条消息,此时队列排队数目为1,而队列之后的延时器还没有启动。根据这两个条件,由“第一条消息控制流程”进行判断,并向队列节点输出“trigger”,立即释放排队的消息
  3. 队列释放消息之后,消息一方面去调用TTS的语音播报。同时触发“正常队列控制流程”,并由构造语音延时节点计算消息的字符数字、可能播报的时间。这个播报时间将控制后续的delay节点
  4. delay节点进行延时,直到“当前语音播报完成”,延时结束
  5. delay节点计时结束之后,TTS播报延时触发队列trigger节点生成控制信息,触发队列释放下一个消息

第一条信息控制流程。由于语音延时控制,是队列的后续流程。当第一条消息进入队列时,控制流程还没有启动,其还无法控制队列的消息释放。所以,必须引入一个不依赖于队列流程的独立流程,该流程将独立判断队列中的消息是否为第一条消息,如果是,则触发队列释放流程,并由此驱动队列及其控制流程开始运转。下面是“第一条消息控制流程”的过程

  1. status节点查询队列节点和延时节点的状态
  2. 延时节点状态为“blue”,说明它正在进行延时,否则,它就是闲置。如果它闲置,我们就构建一个标识,并把这个标识存入flow内,以便其他流程可引用
  3. 队列节点的状态可查询队列中的排队数量。如果队列中的消息只有一条,并且延时器又没有工作,我们就认为这是第一条消息,就可以向队列发出“trigger”,触发队列立即释放一条消息。而如果不是上面的条件,该流程不会发出任何消息,也就不会影响“正常队列控制流程”的正常运转。

控制流程如图

image-20200420125759743

流程json

  1. 节点说明:这个流中,我加入了一些说明。比如,在“tts播报数据”节点的说明中包括了播报数据的样式;在一些比较关键的节点的说明中,也都包含了基本说明。
  2. “TTS播报排队”之前的一堆节点,是队列节点的控制示例,可以手动控制队列的释放、刷新、清空、查看状态等操作。
  3. “语音播报”节点是配置了hass的服务,调用hass实现tts的语音播报。
[{"id":"ff30ab2b.0a6e38","type":"api-call-service","z":"ce9daf41.51c38","name":"语音播报","server":"bab09590.5967f8","version":1,"debugenabled":true,"service_domain":"tts","service":"baidu_say","entityId":"","data":"","dataType":"json","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":780,"y":480,"wires":[[]]},{"id":"4d0d7d81.dd3c44","type":"function","z":"ce9daf41.51c38","name":"tts播报数据","func":"Voice_information = msg.payload.Voice_information || [];\nentity_id = msg.payload.entity_id || [];\nreturn_id = msg.payload.return_id || [];\naa = new Date();\n    y1 = aa.getFullYear();       //转日期格式之后,提取年,数值型\n    m1 = aa.getMonth() + 1;     //转日期格式之后,提取月,数值型\n    d1 = aa.getDate();          //转日期格式之后,提取日,数值型\n    h1 = aa.getHours();         //转日期格式之后,提取时,数值型\n    m1 = aa.getMinutes();       //转日期格式之后,提取分,数值型\ntime = y1 + \"年\" + (m1 < 10 ? \"0\" + m1 : m1) + \"月\" + (d1 < 10 ? \"0\" + d1 : d1) + \"日\" + h1 + \"点\" + m1;\n\n\nif(entity_id == \"\"){\n    a = \"播报时间:\" + time + \"。您没有指定输出设备,我们为您在media_player.xiao_du_zhi_neng_yin_xiang_2811上进行播报\";\n    entity_id = \"media_player.xiao_du_zhi_neng_yin_xiang_2811\";\n}else a = \"播报时间:\" + time + \"。我们将在\"+entity_id+\"为您播报\"\nglobal.set(return_id,a);\n\nmsg.payload = {};\nmsg.payload.data = {\n    \"entity_id\": entity_id,\n    \"message\":Voice_information,\n    \"cache\":\"false\"}\nreturn msg;\n","outputs":1,"noerr":0,"x":430,"y":480,"wires":[["dba6512.54206b"]],"info":"前端流程输出数据的样式:\n\nentity_id = \"\";\n//entity_id = \"media_player.xiao_du_zhi_neng_yin_xiang_1209\";\n\n//填充您的播放内容。如果为空,将不会有任何播报\nVoice_information = \"TTS语音播报使用的hass下比较简单的dlna音响播放器,当前该播放器存在一个比较大的问题是,当前一个语音播报尚未完成时,如果插入一条新播报消息,则新消息会被立即播报,而前一条消息会被立即撤销。当一个语音设备输入来源较多时,就会比较大的冲突,很容易漏掉关键消息。播放消息的不完整,使得体验非常差。所以,需要引入队列控制流程,在语音消息未播放完成,后续的消息进入队列排队,一条一条进行播放。;\nmsg.payload = {return_id,entity_id,Voice_information};\nreturn msg;"},{"id":"dba6512.54206b","type":"q-gate","z":"ce9daf41.51c38","name":"TTS播报排队","controlTopic":"control","defaultState":"queueing","openCmd":"open","closeCmd":"close","toggleCmd":"toggle","queueCmd":"queue","defaultCmd":"default","triggerCmd":"trigger","flushCmd":"flush","resetCmd":"reset","peekCmd":"peek","dropCmd":"","statusCmd":"status","maxQueueLength":"5","keepNewest":false,"qToggle":false,"persist":false,"x":430,"y":680,"wires":[["c3da0609.383518","ff30ab2b.0a6e38"]]},{"id":"7595fcfe.7a4724","type":"inject","z":"ce9daf41.51c38","name":"input","topic":"","payload":"{\"msg.payload.data.message\":123}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":90,"y":680,"wires":[["dba6512.54206b"]]},{"id":"fcf101ff.ba35d","type":"inject","z":"ce9daf41.51c38","name":"open","topic":"control","payload":"open","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":500,"wires":[["dba6512.54206b"]]},{"id":"48174783.9fe4c8","type":"inject","z":"ce9daf41.51c38","name":"toggle","topic":"control","payload":"toggle","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":580,"wires":[["dba6512.54206b"]]},{"id":"f8f59e98.2e0f6","type":"inject","z":"ce9daf41.51c38","name":"close","topic":"control","payload":"close","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":540,"wires":[["dba6512.54206b"]]},{"id":"c91b61.566b44a","type":"inject","z":"ce9daf41.51c38","name":"default","topic":"control","payload":"default","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":620,"wires":[["dba6512.54206b"]]},{"id":"a559ef0f.b5e36","type":"inject","z":"ce9daf41.51c38","name":"queue","topic":"control","payload":"queue","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":740,"wires":[["dba6512.54206b"]]},{"id":"96db7e23.e8768","type":"inject","z":"ce9daf41.51c38","name":"flush","topic":"control","payload":"flush","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":820,"wires":[["dba6512.54206b"]]},{"id":"7a55a294.33ff0c","type":"inject","z":"ce9daf41.51c38","name":"trigger","topic":"control","payload":"trigger","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":780,"wires":[["dba6512.54206b"]]},{"id":"3f2ea7a8.00ebe8","type":"inject","z":"ce9daf41.51c38","name":"reset","topic":"control","payload":"reset","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":860,"wires":[["dba6512.54206b"]]},{"id":"44166be1.a98cd4","type":"inject","z":"ce9daf41.51c38","name":"status","topic":"control","payload":"status","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":900,"wires":[["dba6512.54206b"]]},{"id":"c3da0609.383518","type":"function","z":"ce9daf41.51c38","name":"构造语音延时trigger","func":"a = msg.payload.data.message;\n//a = msg.payload;\nb = a.length;\nc = b * 248;\n\nmsg.delay = c;\nreturn msg;","outputs":1,"noerr":0,"x":640,"y":680,"wires":[["ea3c1b84.e6bbf8"]]},{"id":"ea3c1b84.e6bbf8","type":"delay","z":"ce9daf41.51c38","name":"","pauseType":"delayv","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":830,"y":680,"wires":[["dfcbcb3d.99b338"]]},{"id":"dfcbcb3d.99b338","type":"function","z":"ce9daf41.51c38","name":"TTS播报延时后触发队列trigger","func":"msg.topic = \"control\";\nmsg.payload = \"trigger\";\nreturn msg;","outputs":1,"noerr":0,"x":1050,"y":680,"wires":[["dba6512.54206b"]]},{"id":"55bf2d91.d67c74","type":"status","z":"ce9daf41.51c38","name":"查看队列和延时器状态","scope":["dba6512.54206b","ea3c1b84.e6bbf8"],"x":440,"y":860,"wires":[["f83350f3.5faea"]]},{"id":"42a53259.a127dc","type":"function","z":"ce9daf41.51c38","name":"开启第一条消息trigger","func":"a = msg.status;\na_key = flow.get('TTS_queue_delay') || 0;\nfill = a.fill;\ntext = a.text;\nshape = a.shape;\nid = a.source.id;\n\nif(id == \"dba6512.54206b\"&&fill == \"yellow\"&&shape == \"ring\"){\n    num = text.substr(9,1) * 1\n}\n\nif(num == 1&&a_key == 0){\n    msg.topic = \"control\"\n    msg.payload = \"trigger\"\n    return msg;\n} \n//msg.payload = num;\n","outputs":1,"noerr":0,"x":1020,"y":840,"wires":[["dba6512.54206b"]]},{"id":"f83350f3.5faea","type":"switch","z":"ce9daf41.51c38","name":"","property":"status.source.type","propertyType":"msg","rules":[{"t":"eq","v":"q-gate","vt":"str"},{"t":"eq","v":"delay","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":3,"x":610,"y":860,"wires":[["9205bad3.b44428"],["94d29d13.fc60d"],[]]},{"id":"5711afbd.ea9d","type":"debug","z":"ce9daf41.51c38","name":"队列延时器状态","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":1000,"y":920,"wires":[]},{"id":"94d29d13.fc60d","type":"function","z":"ce9daf41.51c38","name":"保存延时器状态","func":"var a_key = 0;\nflow.set('TTS_queue_delay',a_key);\nif(msg.status.fill == \"blue\"){\n    a_key = 1\n    flow.set('TTS_queue_delay',a_key);\n}\nmsg.payload = a_key;\nreturn msg;","outputs":1,"noerr":0,"x":780,"y":900,"wires":[[]]},{"id":"9205bad3.b44428","type":"delay","z":"ce9daf41.51c38","name":"等待延时状态刷新","pauseType":"delay","timeout":"200","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":790,"y":840,"wires":[["42a53259.a127dc"]]},{"id":"b95eafaf.10522","type":"debug","z":"ce9daf41.51c38","name":"TTS队列状态","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":1030,"y":880,"wires":[]},{"id":"9e6376ed.1cf878","type":"comment","z":"ce9daf41.51c38","name":"正常队列控制流程","info":"当消息传入后,为了防止前面的消息没有播放完成,后一个消息插入,截断了尚未完成播报的消息。所以在播报之前,首先进入消息排队。等待前一个消息完成之后,后一个消息再进入播报环节。过程是,\n1. 一个新消息到来,进入排队\n2. 前一个消息播放完成,排队控制部分发出“trigger”,释放排队最前面的消息\n3. 以此类推,一个消息播放完成,排队控制部分就发出一个trigger,释放队列中的消息\n\n排队控制分为两部分。一部分是正常的队列控制,一部分是第一个消息的释放控制。\n正常队列控制:\n1. 队列释放出的消息,触发控制流程\n2. 首先计算消息播放的时间,并输出给delay节点\n3. delay节点延时\n4. 延时结束之后,TTS队列节点输出trigger,给队列节点,释放队列中的最早一个消息\n\n第一个消息的释放控制:\n1. 通过status节点,查询延时器节点状态和队列节点状态\n2. 延时器状态为闲置时,发出一个flow标识\n3. 队列节点状态经过200ms延时,等待延时器状态的识别完成\n4. 两个状态进行比较,当符合第一个消息的条件时,输出一个trigger,控制队列立即释放消息\n第一个消息释放流程,饶过了正常控制队列流程,其目的是使得第一条消息可以被立即释放。只有第一条消息被队列释放出来,才有可能触发后续的正常队列控制流程发挥作用\n","x":670,"y":640,"wires":[]},{"id":"ada83ba7.9a6148","type":"comment","z":"ce9daf41.51c38","name":"第一条消息控制流程","info":"第一个消息的释放控制:\n1. 通过status节点,查询延时器节点状态和队列节点状态\n2. 延时器状态为闲置时,发出一个flow标识\n3. 队列节点状态经过200ms延时,等待延时器状态的识别完成\n4. 两个状态进行比较,当符合第一个消息的条件时,输出一个trigger,控制队列立即释放消息\n第一个消息释放流程,饶过了正常控制队列流程,其目的是使得第一条消息可以被立即释放。只有第一条消息被队列释放出来,才有可能触发后续的正常队列控制流程发挥作用\n","x":430,"y":800,"wires":[]},{"id":"bab09590.5967f8","type":"server","z":"","name":"Home Assistant","legacy":false,"addon":false,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true}]

我是一个纯的开发小白,很多语句是和大神们借抄过来的。其中肯定会有理解错误的,更可能会有使用错误的。请大佬们多多指点。

下下来看了下,好复杂,看不懂,楼主这是大神级别啊。

:smile:也是被逼的,这个dlna没有排队控制,播报多了就太乱了。我估计应该有更好的方案
其实方案都是围绕着q-gate这个排队节点再转。要是可以得到播放器的状态,那就是太简单了。正在play就排队,如果idle了就放行。

小爱同学的tts好用太多。推荐试试

大佬,我也想用啊。小度可以使用吗?我一直找小度可用的tts呢

要有小爱非触屏音箱才行哦

:smile:感觉我们这些小度被抛弃了。但说实话,小度音箱其他方面还是很不错的。就是这个dlna,估计是制式、编码什么的和别人不一样。直到新版hass才将将可用,但问题也不少。之前的hass版本是播了就停不下来,或者不再接收新的消息,就死鱼一样赖着不动。新版hass总算能动了,也能播放下一条TTS了,但是状态完全不对,显示一直在播放,好在还是可以用了。