Skip to content

B.1 TTS Notification

H. Klages edited this page Apr 15, 2024 · 3 revisions

Introduction

To play TTS notification on SONOS-Player there exist already an excellent solution: TTS Ultimate. This package provides all components:

  • converts text to speech and creates a mp3 file

  • uses a cache to reduce the amount of aws (Amazon) requests

  • serves the local mp3 file to enable the SONOS-Player to access it

  • queues all requests to avoid mixing up notifications

  • plays the notification on a SONOS-Player

TTS-Ultimate is a complete and reliable package and I recommend it!

For those who want to create their own solution, I wired together the following flows. These flows provide similar functionality but use node-red-contribut-sonos-plus for playing the notifications.

Create your own solution

  1. Get your Amazon AWS credentials (polly read access is needed)

  2. Install the following Node-RED packages

    • node-red-contrib-polly-tts (text to speech with amazon polly)

    • node-red-contrib-fs-ops (file system operation)

    • node-red-contrib-sonos-plus (SONOS-Player control)

  3. Import the attached flow

  4. Enter credentials for polly tts

  5. Update the config node with your player data (best ip address)

  6. Update the setup change nodes (your ip address, directories) and start the flow

Keep in mind that you need to save the globals (setting.js) or have to set up after any flow restart.

  1. Define your own text and use as topic always notification_<shortcut>.mp3 - otherwise, you have to change the flows.

CAUTION In case you use Node-RED projects and publish them to the internet you should enable encryption in project settings. Otherwise, your Amazon key may be exposed to the internet.

The Flow

tts

Flow: [{"id":"09166051c976893e","type":"inject","z":"726a8484a6779744","name":"weather","props":[{"p":"payload"},{"p":"topic","vt":"str"},{"p":"language","v":"English","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"notification_weather.mp3","payload":"We expect nice weather without any rain. Enjoy the day.","payloadType":"str","x":91,"y":545,"wires":[["ece2125120bccc5b"]]},{"id":"8e6f82095d70b547","type":"change","z":"726a8484a6779744","name":"prepare","rules":[{"t":"set","p":"payload","pt":"msg","to":"$globalContext('system_baseUrl') & '/notifications/' & topic","tot":"jsonata"},{"t":"set","p":"volume","pt":"msg","to":"40","tot":"num"},{"t":"set","p":"sameVolume","pt":"msg","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":898,"y":620,"wires":[["67eddce4fb72fea8"]]},{"id":"70db87f850d0ab34","type":"fs-ops-copy","z":"726a8484a6779744","name":"","sourcePath":"","sourcePathType":"str","sourceFilename":"file","sourceFilenameType":"msg","destPath":"notification_cache","destPathType":"global","destFilename":"topic","destFilenameType":"msg","link":false,"overwrite":true,"x":759,"y":582,"wires":[["8e6f82095d70b547"]]},{"id":"0bd1385a593303be","type":"switch","z":"726a8484a6779744","name":"","property":"polly.cached","propertyType":"msg","rules":[{"t":"false"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":630,"y":614,"wires":[["70db87f850d0ab34"],["8e6f82095d70b547"]]},{"id":"660dfa3392bb009c","type":"inject","z":"726a8484a6779744","name":"Caution","props":[{"p":"payload"},{"p":"topic","vt":"str"},{"p":"language","v":"English","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"notification_caution.mp3","payload":"Caution Caution Caution","payloadType":"str","x":91,"y":578,"wires":[["ece2125120bccc5b"]]},{"id":"e4a3638294b2d753","type":"inject","z":"726a8484a6779744","name":"inform","props":[{"p":"payload"},{"p":"topic","vt":"str"},{"p":"language","v":"English","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"notification_inform.mp3","payload":"Please mind your steps","payloadType":"str","x":91,"y":611,"wires":[["ece2125120bccc5b"]]},{"id":"f1c755117ca4bd2d","type":"comment","z":"726a8484a6779744","name":"Examples","info":"","x":101,"y":506,"wires":[]},{"id":"a2c01528066a3c33","type":"comment","z":"726a8484a6779744","name":"Create/Update mp3 file, create uri and parameter","info":"","x":501,"y":506,"wires":[]},{"id":"53b3f563d5d7ce3f","type":"comment","z":"726a8484a6779744","name":"Serialize msgs and play notification","info":"","x":1194,"y":505,"wires":[]},{"id":"cbe0b45220a0eab4","type":"inject","z":"726a8484a6779744","name":"Clear storage / cache","props":[{"p":"payload","v":"","vt":"date"},{"p":"topic","v":"","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":1100,"y":280,"wires":[["30e5b09daec650e2","e870064f09ebcdaa"]]},{"id":"fd2cc219db528c26","type":"link in","z":"726a8484a6779744","name":"execute play_notification","links":["67eddce4fb72fea8","76e6ed77.e3dd84"],"x":1068,"y":545,"wires":[["c51a6bd067db745a"]]},{"id":"67eddce4fb72fea8","type":"link out","z":"726a8484a6779744","name":"Trigger play_notification","links":["fd2cc219db528c26"],"x":963,"y":545,"wires":[]},{"id":"19fa078cb70f3443","type":"inject","z":"726a8484a6779744","name":"set","repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":91,"y":154,"wires":[["ea675a176f07b9c0","726d43a5ce207f16","ca378038dfaa2da1"]]},{"id":"3a7e514545008c4b","type":"comment","z":"726a8484a6779744","name":"3. Create mp3 files from text and play on SONOS","info":"","x":200,"y":472,"wires":[]},{"id":"ea675a176f07b9c0","type":"change","z":"726a8484a6779744","name":"","rules":[{"t":"set","p":"system_baseUrl","pt":"global","to":"http://192.168.178.11:1880","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":297,"y":121,"wires":[[]]},{"id":"726d43a5ce207f16","type":"change","z":"726a8484a6779744","name":"","rules":[{"t":"set","p":"notification_directory","pt":"global","to":"\\Users\\hekla\\AppData\\Local\\Temp","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":307,"y":153,"wires":[[]]},{"id":"ca378038dfaa2da1","type":"change","z":"726a8484a6779744","name":"","rules":[{"t":"set","p":"notification_cache","pt":"global","to":"\\Users\\hekla\\AppData\\Local\\Temp","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":297,"y":185,"wires":[[]]},{"id":"cf2ec54def8befe9","type":"change","z":"726a8484a6779744","name":"add cache","rules":[{"t":"set","p":"options.dir","pt":"msg","to":"notification_cache","tot":"global"}],"action":"","property":"","from":"","to":"","reg":false,"x":628,"y":545,"wires":[["0d2776fcb81f78cf"]]},{"id":"11bfe6b87295e8bd","type":"link in","z":"726a8484a6779744","name":"TTS Execute","links":["18152db0.dc8512","ece2125120bccc5b","26ac2b2.49d16d4","4613a740.f0ce58"],"x":338,"y":545,"wires":[["13747f6353133395"]]},{"id":"ece2125120bccc5b","type":"link out","z":"726a8484a6779744","name":"TTS Trigger","links":["11bfe6b87295e8bd"],"x":241,"y":545,"wires":[]},{"id":"8707d5313c596930","type":"comment","z":"726a8484a6779744","name":"Maintain","info":"","x":1060,"y":245,"wires":[]},{"id":"a9daafbd44b8ea14","type":"fs-ops-delete","z":"726a8484a6779744","name":"","path":"notification_directory","pathType":"global","filename":"files","filenameType":"msg","x":1220,"y":319,"wires":[[]]},{"id":"30e5b09daec650e2","type":"fs-ops-dir","z":"726a8484a6779744","name":"","path":"notification_directory","pathType":"global","filter":"notification*.","filterType":"str","dir":"files","dirType":"msg","x":1070,"y":319,"wires":[["a9daafbd44b8ea14"]]},{"id":"e870064f09ebcdaa","type":"fs-ops-dir","z":"726a8484a6779744","name":"","path":"notification_cache","pathType":"global","filter":"*_.mp3","filterType":"str","dir":"files","dirType":"msg","x":1070,"y":352,"wires":[["08a50b6fc7dcd03d"]]},{"id":"08a50b6fc7dcd03d","type":"fs-ops-delete","z":"726a8484a6779744","name":"","path":"notification_directory","pathType":"global","filename":"files","filenameType":"msg","x":1220,"y":352,"wires":[[]]},{"id":"59ac84e6ebd9d93d","type":"status","z":"726a8484a6779744","name":"","scope":["3d7509f22fe8a45f"],"x":1113,"y":594,"wires":[["fd0e65534355072b"]]},{"id":"fd0e65534355072b","type":"change","z":"726a8484a6779744","name":"initiate next","rules":[{"t":"set","p":"releaseNext","pt":"msg","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":1265,"y":594,"wires":[["c51a6bd067db745a"]]},{"id":"c51a6bd067db745a","type":"function","z":"726a8484a6779744","name":"queue","func":"// modified version - originally from https://gist.github.com/dceejay/cea8afa28b7a93ebdc0f \n\n// restartQueue = create new\nif (msg.hasOwnProperty(\"restartQueue\")) {\n context.queue = []\n \n// if queue doesn’t exist, create it \n} else {\n context.queue = context.queue || []\n context.busy = context.busy || false\n\n // if the msg is a trigger one release next message\n if (msg.hasOwnProperty(\"releaseNext\")) {\n if (context.queue.length > 0) {\n var m = context.queue.shift()\n node.status({ fill: 'green', shape: 'dot', text: context.queue.length })\n return m \n }\n else {\n context.busy = false;\n }\n }\n else {\n if (context.busy) {\n // if busy add to queue\n context.queue.push(msg);\n }\n else {\n // otherwise we are empty so just pass through and set busy flag\n context.busy = true;\n node.status({ fill: 'green', shape: 'dot', text: context.queue.length })\n return msg;\n }\n }\n}\nnode.status({ fill: 'green', shape: 'dot', text: context.queue.length })\n\nreturn null;","outputs":1,"noerr":0,"x":1176,"y":545,"wires":[["3d7509f22fe8a45f"]]},{"id":"45179c8356bde68b","type":"http in","z":"726a8484a6779744","name":"","url":"/notifications/:mp3file","method":"get","upload":false,"swaggerDoc":"","x":131,"y":285,"wires":[["cfb24a994878f28a"]]},{"id":"cfb24a994878f28a","type":"change","z":"726a8484a6779744","name":"","rules":[{"t":"set","p":"filename","pt":"msg","to":"$globalContext('notification_directory') & '/' & msg.req.params.mp3file","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":355,"y":285,"wires":[["655cba903130e99f","06ebcf9458ac45dd"]]},{"id":"655cba903130e99f","type":"file in","z":"726a8484a6779744","name":"","filename":"filename","filenameType":"msg","format":"","chunk":false,"sendError":false,"encoding":"base64","x":549,"y":285,"wires":[["ce764ff97407fc0b"]]},{"id":"28b42f928a054174","type":"catch","z":"726a8484a6779744","name":"","scope":["cfb24a994878f28a","655cba903130e99f","ce764ff97407fc0b"],"uncaught":false,"x":553,"y":333,"wires":[["11e6aad395f6d328","c7bfac6252f0a4c0"]]},{"id":"abd0ba24f05706e0","type":"http response","z":"726a8484a6779744","name":"","statusCode":"","headers":{},"x":833,"y":284,"wires":[]},{"id":"15e0687b737da2d4","type":"inject","z":"726a8484a6779744","name":"Flush command queue","repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":1100,"y":420,"wires":[["1d5cd7d3bcaa5bbe"]]},{"id":"1d5cd7d3bcaa5bbe","type":"change","z":"726a8484a6779744","name":"","rules":[{"t":"set","p":"restartQueue","pt":"msg","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":1112,"y":457,"wires":[["c51a6bd067db745a"]]},{"id":"13747f6353133395","type":"function","z":"726a8484a6779744","name":"check globals","func":"// checks existens of requried global variables\n//\nlet value = global.get('system_baseUrl')\nif (!isTruthyAndNotEmptyString(value)) {\n node.error('global - system_baseUrl not defined')\n return null\n}\nvalue = global.get('notification_cache')\nif (!isTruthyAndNotEmptyString(value)) {\n node.error('global - notification_cache not defined')\n return null\n}\nvalue = global.get('notification_directory')\nif (!isTruthyAndNotEmptyString(value)) {\n node.error('global - notification_directory not defined')\n return null\n}\nreturn msg\n\nfunction isTruthyAndNotEmptyString (input) {\n return !(typeof input === 'undefined' || input === null ||\n (typeof input === 'number' && !Number.isFinite(input)) || input === '')\n}","outputs":1,"noerr":0,"initialize":"","finalize":"","x":455,"y":545,"wires":[["cf2ec54def8befe9"]]},{"id":"1494e36c459a9eb7","type":"inject","z":"726a8484a6779744","name":"Just play file","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"notification_weather.mp3","x":814,"y":659,"wires":[["8e6f82095d70b547"]]},{"id":"ce764ff97407fc0b","type":"change","z":"726a8484a6779744","name":"set 200","rules":[{"t":"set","p":"statusCode","pt":"msg","to":"200","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":687,"y":285,"wires":[["abd0ba24f05706e0"]]},{"id":"11e6aad395f6d328","type":"change","z":"726a8484a6779744","name":"set 404","rules":[{"t":"set","p":"statusCode","pt":"msg","to":"404","tot":"num"},{"t":"set","p":"payload","pt":"msg","to":"error","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":689,"y":333,"wires":[["abd0ba24f05706e0"]]},{"id":"c7bfac6252f0a4c0","type":"debug","z":"726a8484a6779744","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":674,"y":368,"wires":[]},{"id":"723006f9cb14b7cb","type":"debug","z":"726a8484a6779744","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":1490,"y":594,"wires":[]},{"id":"93ad2b1df20a1604","type":"inject","z":"726a8484a6779744","name":"habit","props":[{"p":"payload","v":"Don’t do unto others what you would not have done unto you. Don’t do unto others what you would not have done unto you.","vt":"str"},{"p":"topic","v":"notification_habit.mp3","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"notification_habit.mp3","payload":"Don’t do unto others what you would not have done unto you. Don’t do unto others what you would not have done unto you.","payloadType":"str","x":91,"y":644,"wires":[["ece2125120bccc5b"]]},{"id":"188a506e52f8e38e","type":"inject","z":"726a8484a6779744","name":"windig deutsch","props":[{"p":"payload"},{"p":"topic","vt":"str"},{"p":"language","v":"German","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"notification_windy.mp3","payload":"Es ist windig draußen!","payloadType":"str","x":121,"y":677,"wires":[["ece2125120bccc5b"]]},{"id":"a7c6dfd59ca2b6c4","type":"polly","z":"726a8484a6779744","name":"","voice":"0","ssml":false,"dir":"overwritten","config":"8945d5a5.31aef8","x":513,"y":599,"wires":[["0bd1385a593303be"],[]]},{"id":"3d7509f22fe8a45f","type":"sonos-universal","z":"726a8484a6779744","confignode":"88e9fa70.4a53e8","command":"group.play.notification","state":"","stateType":"str","avoidCheckPlayerAvailability":false,"name":"","x":1361,"y":545,"wires":[["723006f9cb14b7cb"]]},{"id":"06ebcf9458ac45dd","type":"debug","z":"726a8484a6779744","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":343,"y":347,"wires":[]},{"id":"9e0acc916fa026f0","type":"comment","z":"726a8484a6779744","name":"READ ME: Http fileserver and TTS","info":"PURPOSE: \nPlay a notification on a SONOS group with content given by text.\n\nMETHOD\nFor a given text (english, german) the polly nodes creates a mp3 file \nand stores that in the cache = \"notification_cache\" directory. \nThe file name is the \"given text\". Polly uses the name to decide\nif it can reuse existing files \n\nThe flow copies that files to the \"notification directory\" \nand renames it (according to name given in msg.topic) \n\nBoth diretory have to set and stored in the corresponding \nglobal variables.\n\nThe build in Node-RED http server is used to serve the mp3 files\nin the local network and SONOS player is able to use that. It reads\nthe file from the \"notification_directory)\n\nThe url and the path has to be defined and stored in the global\nvariable system_baseUrl\n\n---\n# 1. Set directories and http server base url\nWe have to define \na) the cache directory for polly (location of mp3 file)\nb) the notification directory (where http server gets the mp3)\nc) base url for the http server\n\n# 2. http server\n\nFirst we need a http server. This is build into Node-RED \nand can be accessed via the the \"http in\" node. \nSee flow 1. Serving files via http on local system\n\nAn example is: \nhttp://192.168.178.11:1880/notifications/notification_weather.mp3\n\nwhere 192.168.178.11 is the ip address of the node-red system\nwhere notification is just a path to help organizing api calls\nwhere \"notification_weather.mp3\" is a phyiscal file. \n\n# 3. Create mp3 files from text and play on SONOS\npayload: text\ntopic: filename to be used for Serving\nlanguage: German or English \n\n\n\n\n\n\n","x":145,"y":37,"wires":[]},{"id":"ba0154bee64c2758","type":"comment","z":"726a8484a6779744","name":"2. Serving files via http on local system - path includes notification!","info":"First we need a http server. This is build in into Node-RED \nand can be accessed via the the \"http in\" node. \n\n","x":260,"y":245,"wires":[]},{"id":"567aa03687b1c012","type":"polly","z":"726a8484a6779744","name":"","voice":"41","ssml":false,"dir":"overwritten","config":"8945d5a5.31aef8","x":513,"y":639,"wires":[["0bd1385a593303be"],[]]},{"id":"0d2776fcb81f78cf","type":"switch","z":"726a8484a6779744","name":"DE ? US","property":"language","propertyType":"msg","rules":[{"t":"eq","v":"English","vt":"str"},{"t":"eq","v":"German","vt":"str"}],"checkall":"false","repair":false,"outputs":2,"x":390,"y":616,"wires":[["a7c6dfd59ca2b6c4"],["567aa03687b1c012"]]},{"id":"9fc777341de978e5","type":"comment","z":"726a8484a6779744","name":"1. Set directory names and http ip/port - AS GLOBAS (prereq: persistant)","info":"","x":280,"y":80,"wires":[]},{"id":"8945d5a5.31aef8","type":"polly-config","name":"pollyaws"},{"id":"88e9fa70.4a53e8","type":"sonos-config","name":"your_player_dns","serialnum":"","ipaddress":"sonoskitchen.fritz.box"}]

Clone this wiki locally