TTS queue in Node Red for Google Cloud (and other) TTS Engine

Hello guys,

the is a problem with sending TTS messages to speakers because if you send one message before another finished, first one will be interrupted.

Here is a Flow that sovles this problem:
image

So looks Subflow:

Input is

msg.payload.message - text message to speak (example: Hey, lets check the TTS queue!)
msg.payload.speaker - media entity of speaker (example: media_player.google_home_mini_livingroom)

Here is Flow

[{"id":"6f575682.81c368","type":"subflow","name":"Send to TTS","info":"","category":"","in":[{"x":65,"y":120,"wires":[{"id":"5ed64d1.143ceb4"}]}],"out":[],"env":[],"color":"#DDAA99"},{"id":"ef0df12a.430d5","type":"debug","z":"6f575682.81c368","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":905,"y":105,"wires":[]},{"id":"5ed64d1.143ceb4","type":"function","z":"6f575682.81c368","name":"Simple triggered queue","func":"// if queue doesn't exist, create it\ncontext.queue = context.queue || [];\ncontext.busy = context.busy || false;\n\n// if the msg is a trigger one release next message\nif (msg.hasOwnProperty(\"trigger\")) {\n    if (context.queue.length > 0) {\n        var m = context.queue.shift();\n        return {payload:m};\n    }\n    else {\n        context.busy = false;\n    }\n}\nelse {\n    if (context.busy) {\n        // if busy add to queue\n        context.queue.push(msg.payload);\n    }\n    else {\n        // otherwise we are empty so just pass through and set busy flag\n        context.busy = true;\n        return msg;\n    }\n}\n\nreturn null;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":260,"y":120,"wires":[["167e0e74.2831a2"]]},{"id":"a321ec48.9fd84","type":"function","z":"6f575682.81c368","name":"set trigger","func":"// handle the return from the exec in here \n// if all is good then set msg.trigger property to exist\nmsg.trigger = 1;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":210,"y":240,"wires":[["5ed64d1.143ceb4"]]},{"id":"4f7e3bd4.1641c4","type":"api-current-state","z":"6f575682.81c368","name":"Speaker IDLE?","server":"63f11f83.adf4c","version":1,"outputs":2,"halt_if":"idle","halt_if_type":"str","halt_if_compare":"is","override_topic":false,"entity_id":"media_player.google_home_mini_livingroom","state_type":"str","state_location":"","override_payload":"none","entity_location":"","override_data":"none","blockInputOverrides":false,"x":680,"y":195,"wires":[["ef0df12a.430d5","bb1af182.a4b3d"],["eb3f920e.d527c"]]},{"id":"eb3f920e.d527c","type":"delay","z":"6f575682.81c368","name":"","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":660,"y":165,"wires":[["4f7e3bd4.1641c4"]]},{"id":"43aa6bd0.9375a4","type":"api-current-state","z":"6f575682.81c368","name":"Speaker OFF?","server":"63f11f83.adf4c","version":1,"outputs":2,"halt_if":"off","halt_if_type":"str","halt_if_compare":"is","override_topic":false,"entity_id":"media_player.google_home_mini_livingroom","state_type":"str","state_location":"","override_payload":"none","entity_location":"","override_data":"none","blockInputOverrides":false,"x":470,"y":165,"wires":[["6c4b258a.50c4fc"],["4f7e3bd4.1641c4"]]},{"id":"6c4b258a.50c4fc","type":"delay","z":"6f575682.81c368","name":"","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":670,"y":120,"wires":[["bb1af182.a4b3d"]]},{"id":"167e0e74.2831a2","type":"delay","z":"6f575682.81c368","name":"","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":460,"y":120,"wires":[["43aa6bd0.9375a4"]]},{"id":"bb1af182.a4b3d","type":"api-call-service","z":"6f575682.81c368","name":"Google say","server":"63f11f83.adf4c","version":1,"debugenabled":false,"service_domain":"tts","service":"aaa_google_cloud_say","entityId":"","data":"{\"entity_id\":\"{{payload.speaker}}\",\"message\":\"{{payload.message}}\"}","dataType":"json","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":895,"y":195,"wires":[["a321ec48.9fd84"]]},{"id":"e1449c98.5f274","type":"inject","z":"6f575682.81c368","name":"Reset Queue","props":[{"p":"trigger","v":"1","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"","x":115,"y":30,"wires":[["5ed64d1.143ceb4"]]},{"id":"991d20e4.095d6","type":"comment","z":"6f575682.81c368","name":"input data","info":"msg.payload.message - text message to speak\nmsg.payload.speaker - media entity of speaker","x":105,"y":75,"wires":[]},{"id":"63f11f83.adf4c","type":"server","z":"","name":"Home Assistant","legacy":false,"addon":false,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true},{"id":"980f29c.49036d8","type":"inject","z":"2e070ed2.a68682","name":"","props":[{"p":"payload.message","v":"Hey, let's check the TTS queue!","vt":"str"},{"p":"payload.speaker","v":"media_player.google_home_mini_livingroom","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"","x":140,"y":75,"wires":[["d55a17e5.e696b8"]]},{"id":"d55a17e5.e696b8","type":"subflow:6f575682.81c368","z":"2e070ed2.a68682","name":"","env":[],"x":295,"y":75,"wires":[]}]

Just inject so fast and so many times as you want :sunglasses:

Have a nice day!

PS
I forgot to say, that if you have problem with changing of default language in Cloud TTS (mostly when using Nabu Casa Cloud), you need to change service to service_name: aaa_google_cloud_say. My config is:

tts:
  - platform: google_translate
 
  - platform: google_cloud
    service_name: aaa_google_cloud_say
    key_file: key.json
    language: ru-RU
    gender: female
    voice: ru-RU-Wavenet-A
    encoding: linear16
    #speed: 0.9
    #pitch: -2.5
    gain: 5.0
    profiles:
      - small-bluetooth-speaker-class-device

Because of this i have here “aaa_google_cloud_say”


and not just “cloud_say”.

3 Likes

Total noob question but how do input the msg.payload.message and msg.payload.speaker?

I have edited the subflow to reflect my speakers and then I set up two template nodes (Json format) with msg.payload.message and msg.payload.speaker as the topics, with just a “hello” and “media_player.volumio” in each between the inject and Send to TTS nodes.

A debug node shows the message is being generated but there is nothing running through the subflow that i can see and nothing comes out of the speaker

You can do it in may ways for example using change node

Awesome thank you, I’ll give that a go tonight.
I have tried reading the node red docs but it’s a bit overwhelming

So first off thanks for taking the time to answer my questions. From memory your suggestion did work for simple text notifications but i found that trying to reference sensor data just read out the information including the moustache brackets.

I got a bit side tracked trying to get a local tts engine working, using someone elses queuing sequence and discovered that you had to use a get template node {states ('sensor.mysensorname')} in order to phase the sensor data from home assistant or use something like {{entity.sensor.mysensorname}}. However i found picotts a bit muddy and mary tts a big laggy, like drunk sounding?

So i have come back to your sequence using google tts again. Using the inject node provided i can hear the test message once set to my media player ie mopidy. However using the change node i now get an error message in the debug panel telling me that payload.message and payload.speaker are a non object type and cant be set, and i get no sound.

In the end i was planning to use a traffic node with room assistant and some ibeacons as a condition to choose which speaker to play the notifications from. So i modified the call service node removing the {entity_id: {payload.speaker}} and just set it under entity section removing the need to set a payload.speaker and changed the message to ``` {message: {{payload}}}`` I get sound this way but for some reason the queuing no longer works

Any ideas on what i might be doing wrong in either of these cases?

Sorry if I ask… this google_cloud platform it’s free or I must pay ?
Thanks

Minor update to the flow because the entity was used instead of payload.speaker

[{"id":"fe371c3.595d8e","type":"subflow","name":"tts queue","info":"","category":"","in":[{"x":40,"y":40,"wires":[{"id":"8a56d075.090ba"}]}],"out":[{"x":880,"y":166,"wires":[{"id":"cc24107e.1c016","port":0}]}],"env":[],"color":"#DDAA99"},{"id":"859819e0.8b1d78","type":"function","z":"fe371c3.595d8e","name":"Simple triggered queue","func":"// if queue doesn't exist, create it\ncontext.queue = context.queue || [];\ncontext.busy = context.busy || false;\n\n// if the msg is a trigger one release next message\nif (msg.hasOwnProperty(\"trigger\")) {\n    if (context.queue.length > 0) {\n        var m = context.queue.shift();\n        return {payload:m};\n    }\n    else {\n        context.busy = false;\n    }\n}\nelse {\n    if (context.busy) {\n        // if busy add to queue\n        context.queue.push(msg.payload);\n    }\n    else {\n        // otherwise we are empty so just pass through and set busy flag\n        context.busy = true;\n        return msg;\n    }\n}\n\nreturn null;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":350,"y":100,"wires":[["aee9eee8.47a4d8"]]},{"id":"2793342e.dfc574","type":"function","z":"fe371c3.595d8e","name":"set trigger","func":"// handle the return from the exec in here \n// if all is good then set msg.trigger property to exist\nmsg.trigger = 1;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":290,"y":340,"wires":[["859819e0.8b1d78"]]},{"id":"925ecd78.615938","type":"api-current-state","z":"fe371c3.595d8e","name":"Speaker IDLE?","server":"4a86ba78.935684","version":1,"outputs":2,"halt_if":"idle","halt_if_type":"str","halt_if_compare":"is","entity_id":"{{payload.speaker}}","state_type":"str","blockInputOverrides":false,"override_topic":false,"state_location":"","override_payload":"none","entity_location":"","override_data":"none","x":480,"y":160,"wires":[["cc24107e.1c016"],["f1e12209.95fc08"]]},{"id":"f1e12209.95fc08","type":"delay","z":"fe371c3.595d8e","name":"","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":480,"y":240,"wires":[["925ecd78.615938"]]},{"id":"85111b30.de727","type":"api-current-state","z":"fe371c3.595d8e","name":"Speaker OFF?","server":"4a86ba78.935684","version":1,"outputs":2,"halt_if":"off","halt_if_type":"str","halt_if_compare":"is","entity_id":"{{payload.speaker}}","state_type":"str","blockInputOverrides":false,"override_topic":false,"state_location":"","override_payload":"none","entity_location":"","override_data":"none","x":620,"y":100,"wires":[["994f136f.5f16"],["925ecd78.615938"]]},{"id":"994f136f.5f16","type":"delay","z":"fe371c3.595d8e","name":"","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":730,"y":40,"wires":[["cc24107e.1c016"]]},{"id":"aee9eee8.47a4d8","type":"delay","z":"fe371c3.595d8e","name":"","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":510,"y":40,"wires":[["85111b30.de727"]]},{"id":"cc24107e.1c016","type":"api-call-service","z":"fe371c3.595d8e","name":"Google say","server":"4a86ba78.935684","version":3,"debugenabled":false,"service_domain":"tts","service":"google_say","entityId":"all","data":"{\"entity_id\":\"{{payload.speaker}}\",\"message\":\"{{payload.message}}\"}","dataType":"json","mergecontext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":770,"y":220,"wires":[["f22f644.0130418"]]},{"id":"54151074.51deb8","type":"inject","z":"fe371c3.595d8e","name":"Reset Queue","props":[{"p":"trigger","v":"1","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"","payloadType":"str","x":210,"y":40,"wires":[["859819e0.8b1d78"]]},{"id":"8a56d075.090ba","type":"delay","z":"fe371c3.595d8e","name":"","pauseType":"random","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":100,"y":100,"wires":[["859819e0.8b1d78"]]},{"id":"f22f644.0130418","type":"ha-wait-until","z":"fe371c3.595d8e","name":"","server":"4a86ba78.935684","outputs":2,"entityId":"input_boolean.lock_notification","entityIdFilterType":"exact","property":"state","comparator":"is","value":"off","valueType":"str","timeout":"30","timeoutType":"num","timeoutUnits":"seconds","entityLocation":"","entityLocationType":"none","checkCurrentState":true,"blockInputOverrides":true,"x":760,"y":286,"wires":[["2793342e.dfc574"],[]]},{"id":"4a86ba78.935684","type":"server","name":"Home Assistant","addon":true},{"id":"11e50e71.175dea","type":"subflow:fe371c3.595d8e","z":"cf937816.c2bd78","name":"","env":[],"x":800,"y":400,"wires":[[]]},{"id":"78298fe.1f795f","type":"inject","z":"cf937816.c2bd78","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":790,"y":320,"wires":[["11e50e71.175dea"]]}]

Here is a node for tts notifications supporting notifications queueing and play priority.
Each notification has it’s own play volume level and the device initial volume is restored after notifications have been played.
You can find this node here:
https://flows.nodered.org/node/node-red-google-notify

Thanks!
Is there a way to use groups speakers?

I can not choose language

sometime this happens, first create your node and then try to edit it again, the languages should then be available

Created node, created one more. Restarted HA… But no language… Overriding also do not help…

can you access the logs of node-red ?

Of course, just show me witch one. Here is nothing for example in Supervisor

it should be displayed like this

But i got only

In other browser the same

on what device are you running node-red, rpi ?

NUC i5 8. Gen, 16Gb, 500Gb SSD

Last versions of all.
image

what other tts or cast nodes have you installed ?

This one