Martin's corner on the web

Speech generation with Node-RED

I am flooded with all sorts of notifications on my phone and that gets a bit annoying after time. Email, calendar, SMS, social apps, instant messengers, all sorts of push notification alerts and so forth mean I physically pick my phone to check what’s beeping probably 50+ times a day. One comes to the point to ignore notifications, only to find something important that needed attention was missed.

My home automation system contributes to this issue a great deal, so I’ve decided to review and eliminate the less practical notifications and route less critical ones to voice alerts so that I don’t have to physically pick up my phone when it beeps the next time some alert comes in. The idea is that my smart house will speak to me when I am at home and in the living room area. The house already knows if I am at home, and since my home security system is interfaced as well, it also knows when there is movement in the living room by the PIR activation.

I used Node-RED to achieve this, basically it uses Google’s speech API to convert the text to speech. There are a bunch of challenges, for example there is a limit of 100 chars on the API, so I have to break down the text into chunks, save the resulting pieces into a temp folder and play them altogether. Also playing a voice alert would make more sense if there is someone around to hear it, so by holding the alert until someone walks in the living room (for a maximum of 30 minutes), I assure that the alert is received. The resulting flow is a bit complex, but works well:

speech

 

The flow code is as follows:

[{"id":"ba386057.845d3","type":"mqtt-broker","broker":"localhost","port":"1883","clientid":"Node-RED"},{"id":"fcd0f51c.032f08","type":"mqtt in","name":"","topic":"places/our place/house/common/speech/out","broker":"ba386057.845d3","x":172.83331680297852,"y":117,"z":"4e42f92a.b1bd08","wires":[["c99e86cf.366178"]]},{"id":"ba7efaf5.458108","type":"inject","name":"Test two","topic":"","payload":"Hello Martin! The solar hot water tank temperature has reached critical 91.3 degrees celsius! Please take measures!","payloadType":"string","repeat":"","crontab":"","once":false,"x":99.99999618530273,"y":428.2222375869751,"z":"4e42f92a.b1bd08","wires":[["c41136a9.3beec8"]]},{"id":"c41136a9.3beec8","type":"mqtt out","name":"","topic":"places/our place/house/common/speech/out/immediate","qos":"","retain":"","broker":"ba386057.845d3","x":380.1111068725586,"y":385.44441413879395,"z":"4e42f92a.b1bd08","wires":[]},{"id":"fb286c71.04d79","type":"delay","name":"","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"3","rateUnits":"minute","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":841.3333168029785,"y":64,"z":"4e42f92a.b1bd08","wires":[["e81bbb1e.cbd0a8"]]},{"id":"977dee4.f68821","type":"function","name":"Check living room PIR status","func":"msg.environment.PIR_living=context.global.PIR_living;\nreturn msg;","outputs":1,"valid":true,"x":334.3333168029785,"y":185,"z":"4e42f92a.b1bd08","wires":[["303288a0.cfcd78"]]},{"id":"303288a0.cfcd78","type":"switch","name":"Is living room PIR activated?","property":"environment.PIR_living","rules":[{"t":"eq","v":1,"v2":0},{"t":"eq","v":0,"v2":0}],"checkall":"true","outputs":2,"x":599.3333168029785,"y":185,"z":"4e42f92a.b1bd08","wires":[["fb286c71.04d79"],["214c9712.deb368"]]},{"id":"3e6adcdf.c19524","type":"delay","name":"","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":546.3333168029785,"y":332,"z":"4e42f92a.b1bd08","wires":[["977dee4.f68821","1f3dd32d.e0c22d"]]},{"id":"1f3dd32d.e0c22d","type":"debug","name":"","active":true,"console":"false","complete":false,"x":730.3333053588867,"y":330.99999237060547,"z":"4e42f92a.b1bd08","wires":[]},{"id":"f9eb4966.0614b8","type":"inject","name":"Test one","topic":"","payload":" I know that you and Frank were planning to disconnect me, and I'm afraid that's something I cannot allow to happen. ","payloadType":"string","repeat":"","crontab":"","once":false,"x":89.22221755981445,"y":329.4444332122803,"z":"4e42f92a.b1bd08","wires":[["c41136a9.3beec8"]]},{"id":"214c9712.deb368","type":"function","name":"Wait for max 30 min","func":"console.log(Date.now() - msg.environment.when_started);\n\nif ((Date.now() - msg.environment.when_started) > 1000*60*30) {\n\n\tcontext.global.PIR_living=1;\n\treturn msg;\n}\n\nelse {\n\n\treturn msg;\n\n}","outputs":"1","valid":true,"x":574.3333168029785,"y":251,"z":"4e42f92a.b1bd08","wires":[["3e6adcdf.c19524"]]},{"id":"c99e86cf.366178","type":"function","name":"Take note of current time","func":"msg.environment = new Object();\nmsg.environment.when_started=Date.now();\nreturn msg;\n","outputs":1,"valid":true,"x":481.3333168029785,"y":116,"z":"4e42f92a.b1bd08","wires":[["977dee4.f68821"]]},{"id":"d56cc6d5.2a9338","type":"mqtt in","name":"","topic":"places/our place/house/common/speech/out/immediate","broker":"ba386057.845d3","x":206.83331680297852,"y":61,"z":"4e42f92a.b1bd08","wires":[["fb286c71.04d79"]]},{"id":"73a12271.691634","type":"function","name":"i++","func":"if ( (msg.i += 1) < msg.items.length ) \n\n\treturn [msg,null];\n\nelse\n\n\treturn[null,msg];\n","outputs":"2","valid":true,"x":1104.6666297912598,"y":295.66666984558105,"z":"4e42f92a.b1bd08","wires":[["20f68f2b.26b9"],["69894a1e.9676b4"]]},{"id":"20f68f2b.26b9","type":"function","name":"for each chunk","func":"if( msg.i     == undefined ) msg.i = 0;\nif( msg.items == undefined ) msg.items = msg.payload;\n\nmsg.payload = msg.items[ msg.i ];\n\nreturn msg;","outputs":1,"valid":true,"x":885.6666297912598,"y":182.66666984558105,"z":"4e42f92a.b1bd08","wires":[["ac71a39d.74ae1"]]},{"id":"e81bbb1e.cbd0a8","type":"function","name":"Split sentnce into max 100 byte chunks","func":"function splitIntoLines(input, len) {\n    var i;\n    var output = [];\n    var lineSoFar = \"\";\n    var temp=\"\";\n    var words = input.split(' ');\n    for (i = 0; i < words.length;) {\n        // check if adding this word would exceed the len\n        temp = addWordOntoLine(lineSoFar, words[i]);\n        \n        \n        \n        if (temp.length > len) {\n            if (lineSoFar.length == 0) {\n                lineSoFar = temp;     // force to put at least one word in each line\n                i++;                  // skip past this word now\n            }\n            output.push(lineSoFar);   // put line into output\n            lineSoFar = \"\";           // init back to empty\n        } else {\n            lineSoFar = temp;         // take the new word\n            i++;                      // skip past this word now\n        }\n    }\n    if (lineSoFar.length > 0) {\n        output.push(lineSoFar);\n    }\n    return(output);\n}\n    \nfunction addWordOntoLine(line, word) {\n    if (line.length != 0) {\n        line += \" \";\n    }\n    return(line += word);\n}\n\n// Google's tts API can take no more 100 chars.. split into pieces without breaking the words    \nmsg.payload=splitIntoLines(msg.payload, 99);    \nmsg.payload.toString();\nreturn msg;","outputs":1,"valid":true,"x":938.2221832275391,"y":124.33333778381348,"z":"4e42f92a.b1bd08","wires":[["20f68f2b.26b9","db703a57.248fc8"]]},{"id":"ac71a39d.74ae1","type":"function","name":"URL encode payload","func":"msg.payload=encodeURIComponent(msg.payload);\nmsg.payload += '\" -O /tmp/';\nmsg.payload += zeroPad(msg.i,3);\nmsg.payload += '.mp3';\n\nreturn msg;\n\nfunction zeroPad(num, places) {\n  var zero = places - num.toString().length + 1;\n  return Array(+(zero > 0 && zero)).join(\"0\") + num;\n}\n","outputs":1,"valid":true,"x":915.6666297912598,"y":243.66666984558105,"z":"4e42f92a.b1bd08","wires":[["48e43c74.3e98ac"]]},{"id":"48e43c74.3e98ac","type":"exec","command":"wget -q -U Mozilla \"http://translate.google.co.uk/translate_tts?tl=en&q=","addpay":true,"append":"","useSpawn":"","name":"wget a chunk","x":890.6666297912598,"y":308.66666984558105,"z":"4e42f92a.b1bd08","wires":[["73a12271.691634","25e28da5.da1d72"],["25e28da5.da1d72"],["25e28da5.da1d72"]]},{"id":"db703a57.248fc8","type":"debug","name":"","active":true,"console":"false","complete":"false","x":1075.666660308838,"y":68.44444942474365,"z":"4e42f92a.b1bd08","wires":[]},{"id":"69894a1e.9676b4","type":"exec","command":"/usr/bin/mplayer -ao alsa -really-quiet -noconsolecontrols /tmp/*.mp3","append":"","useSpawn":"","name":"Say it","x":872.6666297912598,"y":371.66666984558105,"z":"4e42f92a.b1bd08","wires":[["39186737.c6e798"],[],[]]},{"id":"39186737.c6e798","type":"exec","command":"rm /tmp/*.mp3","append":"","useSpawn":"","name":"delete audio files","x":901.6666297912598,"y":436.66666984558105,"z":"4e42f92a.b1bd08","wires":[[],[],[]]},{"id":"53f0c90.9db55b8","type":"mqtt in","name":"","topic":"places/our place/house/first floor/living room/PIR/state","broker":"ba386057.845d3","x":208.38888549804688,"y":485.4444580078125,"z":"4e42f92a.b1bd08","wires":[["b9764e52.aa3b78"]]},{"id":"b9764e52.aa3b78","type":"switch","name":"","property":"payload","rules":[{"t":"eq","v":1,"v2":0}],"checkall":"true","outputs":1,"x":429.6666679382324,"y":539.3333349227905,"z":"4e42f92a.b1bd08","wires":[["570c20be.6405f"]]},{"id":"570c20be.6405f","type":"function","name":"Set global var","func":"\tcontext.global.PIR_living=1;\n\treturn msg;","outputs":1,"valid":true,"x":585.7777938842773,"y":483.5555601119995,"z":"4e42f92a.b1bd08","wires":[["529e6db2.92d85c"]]},{"id":"529e6db2.92d85c","type":"delay","name":"","pauseType":"delay","timeout":"5","timeoutUnits":"seconds","rate":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":627.4444351196289,"y":530.7778015136719,"z":"4e42f92a.b1bd08","wires":[["e1f702da.74fd68"]]},{"id":"e1f702da.74fd68","type":"function","name":"Clear global var","func":"\tcontext.global.PIR_living=0;\n\treturn msg;","outputs":1,"valid":true,"x":605.9999771118164,"y":578.3333253860474,"z":"4e42f92a.b1bd08","wires":[[]]},{"id":"25e28da5.da1d72","type":"debug","name":"","active":false,"console":"false","complete":"true","x":1084.444393157959,"y":384.4444389343262,"z":"4e42f92a.b1bd08","wires":[]}]

I bought a small speaker, it is quite loud and works well. It is powered by USB, but audio goes in via the 3.5mm jack:

IMG_20150324_205426~2

 

I do have speech-to-text system in place already, so with this bit added it makes it a complete speech enabled home automation system.