AWS Polly Speech on RPi4 and RPi5 using Node-Red - Scargill's Tech Blog

AWS Polly is great for adding speech to Raspberry Pi and similar, especially with the “neural” enhancement.Regular readers will know that for years I was a great fan of using Ivona speech on my Raspberry Pi.In Node-Red I used the free Ivona service to provide high quality speech in Node-Red at the heart of my home control setup.

Ivona, good as it was, has been defunct now for some time.  I’m now using Polly successfully on RPi4 and RPi5.I’ve been asked what I use this for? Well, I do of course have Alexa and Google Home, both integrated to some extent into Node-Red and able to control things.However I never RELY on them as they rely completely on connectivity to the web.

So does this – BUT If I need an alert or other phrase that I’ve used previously, of course the code works locally as there’s a local copy of the MP3 file on the RPi already so it then works without Internet.“Pete – time to take your pills” etc.comes to mind.

This post is derived from an original I wrote for Ivona in 2017 but completely overhauled for the (then new new) option in Polly as well as (optionally) including the voice-id (Amy is my favourite) in the input.The Amazon Polly system is effectively a replacement for Ivona.The short, sharp answer is: Polly works, it is effectively free for casual use (<5 million characters a month or 1 million characters with the “neural” option).

It is one of many Amazon AWS services… so CLI access usually begins “aws polly …” Read on, as my simple Node-Red code caches speech recordings (.mp3 files) so that previously-used text does not require successive calls to Amazon servers.The code also allows for buffering of separate messages so they don’t overlap.In case you’re wondering, I could not see a decent AWS POLLY logo/icon on their website so I took the image above from a free-use, no-attribute site So the Amazon “Polly” works via an AWS account.

I have a free Amazon “developer” account and when I tried to add Polly – it said I didn’t have the right permissions – so – I added user pete to my account and made him part of the Polly group – and that didn’t work either – then I noted something about payment and realised I’d not put any payment details in – I did that – and all of a sudden the thing came to life.This has been running for 5-6 years now and I think they’ve charged me maybe 2 or 3 euros in all that time.The way I use Polly is essentially simple  – my code downloads an audio phrase as a .MP3 file given text input and saves it with a meaningful file name.  Next time you want that exact phrase – the code checks to see if the file already exists – if so, it gets played locally, if not, the code gets a new file from AWS Polly and plays that.

In a typical use-case that I for example might have, after a message is used once, it is kept in it’s own file for re-use and hence there’s no chance of incurring significant charges.There are, no doubt, more elegant ways to do this than calling a command line from Node-Red – but this method works perfectly and as far as I know, the result is unique for Node-Red and Polly.Antonio Fragola (Mr Shark) and I have just got this to work on Node-Red in the latest PI OS (Bookworm 64 bit) in a Docker container on both RPi4 and RPi5 (minor changes to the Pi OS built-in amixer settings on RPi5 which also does not have a built-in 3.5mm jack audio out – so I’m using a USB audio dongle on the latter).

I’ll assume you have your aws credentials (ID and secret – simple – free) – don’t worry about location not being where you actually are.Firstly – AWS on Raspberry Pi4 using (32 bit) Pi OS Bullseye To make sure AWS is working, use the command-line (CLI) code below – I’ve used this on RPi2 to 4 without any issues.As user pi, I created a folder called /home/pi/audio to store files… them after not using the speech for a while – I noticed this was failing in 2024 and so updated my Python and re-installed: you should now have Python 3.8 – it may work with later versions but I didn’t try that and installation will fail below Python v3.8 sudo pip install awscli In the AWS initial setup, I set the region to us-west-1 for no other reason than initially not knowing any better – no matter as this gets overwritten in the code below where I use eu-west-2 which works fine with the NEURAL option.

The output format you’d expect to enter might be MP3 but no – so I picked JSON for no good reason in the initial setup – again see the AWS POLLY example below as the Node-Red code overrides some initial settings.Once AWS was installed and with my free AWS access key ID and secret handy, at the command line I used aws configure: pi@ukpi:~ $ aws configure AWS Access Key ID [*********]: AWS Secret Access Key [**********]: Default region name [us-west-1]: Default output format [json]: That done, I tried this test at the command line… aws polly synthesize-speech --output-format mp3 --voice-id Amy --engine "neural" --region eu-west-2 --text "Hello my name is peter." /home/pi/audio/peter.mp3 The resulting file was an .MP3 sitting in my /home/pi/audio folder – this used the voice Amy (British female) to store a phrase into peter.mp3 MPG123 – I downloaded this in the normal manner… sudo apt install mpg123 Amixer – well, on RPi 5, the amixer SEEMS to operate differently to RPi4 so I’ve had to change device (numid) to 6 and from percentages to an actual value where maximum is 400 matching my test inject of 100% – and non-linear down to apparently 0 for 0% etc.in the latter – but for now… ## max volume on RPi4 with internal audio amixer cset numid=1 -- 100% Next step armed with my new recording and the RPi4 3.5mm jack connected to a powered speaker… mpg123 /home/pi/audio/peter.mp3 Sorted – good for testing but as you’ll see the final solution is much better.

The Node-Red sub-flow below is about queuing messages, storing them with meaningful names, playing them back and making sure you don’t re-record a phrase you have already recorded.If you don’t like the default Amy – I’ve included the code to let you add another voice into your input (Brian for example).Important note: In this early part of the article I’m referring to Node-Red in native mode on Raspberry Pi all the way up to Pi OS Bookworm (Lite in my case) – NOT Node-Red in a Docker Container – that is a slightly different setup which I’ll cover further down.

If you want to add sound effects – just put .MP3 files in the audio folder and call them by filename.I have files like red-alert.mp3 and similar using Star Trek recordings.I also have my own .mp3 recordings using Polly.

The first part of my Node-Red code looks to see if the input payload has something in it and if so it pushes that onto a stack.The code then looks to see if any speech is already playing – if not and if there is something on the stack, it checks – if there is an .mp3 file it sends the file to the MP3 player.When it comes to the current request, If there is a matching .mp3, it immediately plays that file (assuming the queue is empty) otherwises the message is sent to aws Polly to create the file – which is downloaded then added to the output queue and then played back with a small delay depending on your broadband speed (for grabbing the .mp3 from the aws servers).

You clearly need your free Amazon account set up and Node-Red for this – you also need the free MPG123 player.Both Node-Red and MPG123 are included in my standard “The Script” – useful for earlier versions of Pi OS before Bookworm.Here is the code I used pre-Bookworm….

the MPG123 exec node simply has mpg123 for the command and append payload ticked.The AWS exec node has aws for the command and append payload ticked.Both exec nodes have the output set to exec mode.

Here is the code for the flow below: Firstly the picture then the flow to include in Node-Red… You can put the code in a Node-Red sub-flow (for ease of use) that can be used by simply injecting some text into the incoming payload.Here however I’ll just show it in a regular flow along with a use-once-at-power-up inject node to reset a couple of global variables so the code knows that Polly is not busy creating an .mp3 file and the MPG123 queuing code knows that no .mp3 file is currently playing.This code is for native installs and for Pi OS before Bookworm.

[ { "id": "c75889b6.c114a8", "type": "subflow", "name": "Volume 0-100", "info": "", "in": [ { "x": 64, "y": 98, "wires": [ { "id": "228f7ec0.797752" } ] } ], "out": [] }, { "id": "bf061a13.249d78", "type": "exec", "z": "c75889b6.c114a8", "command": "amixer cset numid=1 -- ", "addpay": true, "append": "", "useSpawn": "", "name": "Volume", "x": 380, "y": 100, "wires": [ [], [], [] ] }, { "id": "228f7ec0.797752", "type": "function", "z": "c75889b6.c114a8", "name": "Non-linear", "func": "msg.payload/=3;\nif (msg.payload>0) msg.payload+=66;\nmsg.payload+=\"%\";\nreturn msg;", "outputs": 1, "noerr": 0, "x": 210, "y": 100, "wires": [ [ "bf061a13.249d78" ] ] }, { "id": "5c625079.35e59", "type": "comment", "z": "c75889b6.c114a8", "name": "Amixer control for audio", "info": "", "x": 170, "y": 40, "wires": [] }, { "id": "676931a1.872fc", "type": "subflow:c75889b6.c114a8", "z": "947f7b4d.ee6ef8", "x": 770, "y": 160, "wires": [] }, { "id": "1738bb65.3471d5", "type": "inject", "z": "947f7b4d.ee6ef8", "name": "50%", "repeat": "", "crontab": "", "once": false, "topic": "", "payload": "50", "payloadType": "str", "x": 570, "y": 180, "wires": [ [ "676931a1.872fc" ] ] }, { "id": "10e04ec3.32a121", "type": "inject", "z": "947f7b4d.ee6ef8", "name": "25%", "repeat": "", "crontab": "", "once": false, "topic": "", "payload": "25", "payloadType": "str", "x": 570, "y": 140, "wires": [ [ "676931a1.872fc" ] ] }, { "id": "8f283f57.3b399", "type": "inject", "z": "947f7b4d.ee6ef8", "name": "Red Alert", "repeat": "", "crontab": "", "once": false, "topic": "amy", "payload": "red alert", "payloadType": "str", "x": 980, "y": 100, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "2b461c2.7bd40e4", "type": "inject", "z": "947f7b4d.ee6ef8", "name": "Comms beep", "props": [ { "p": "payload", "v": "comms beep", "vt": "str" }, { "p": "topic", "v": "amy", "vt": "string" } ], "repeat": "", "crontab": "", "once": false, "topic": "amy", "payload": "comms beep", "payloadType": "str", "x": 990, "y": 140, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "e928e8.25ad7718", "type": "inject", "z": "947f7b4d.ee6ef8", "name": "Hailing frequencies open", "repeat": "", "crontab": "", "once": false, "topic": "amy", "payload": "hailing frequencies open", "payloadType": "str", "x": 1030, "y": 180, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "2d98b3aa.d7284c", "type": "inject", "z": "947f7b4d.ee6ef8", "name": "Keypress", "props": [ { "p": "payload", "v": "keypress", "vt": "str" }, { "p": "topic", "v": "amy", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "topic": "amy", "payload": "keypress", "payloadType": "str", "x": 980, "y": 220, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "b0d13aff.e60fd8", "type": "inject", "z": "947f7b4d.ee6ef8", "name": "Unable to comply", "props": [ { "p": "payload", "v": "unable to comply", "vt": "str" }, { "p": "topic", "v": "amy", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "topic": "amy", "payload": "unable to comply", "payloadType": "str", "x": 1000, "y": 260, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "7e2cec04.b03ee4", "type": "inject", "z": "947f7b4d.ee6ef8", "name": "0%", "repeat": "", "crontab": "", "once": false, "topic": "", "payload": "0", "payloadType": "str", "x": 570, "y": 100, "wires": [ [ "676931a1.872fc" ] ] }, { "id": "e4d915f1.5aaea8", "type": "inject", "z": "947f7b4d.ee6ef8", "name": "100%", "repeat": "", "crontab": "", "once": false, "topic": "", "payload": "100", "payloadType": "str", "x": 570, "y": 260, "wires": [ [ "676931a1.872fc" ] ] }, { "id": "b98733e4.20171", "type": "inject", "z": "947f7b4d.ee6ef8", "name": "75%", "repeat": "", "crontab": "", "once": false, "topic": "", "payload": "75", "payloadType": "str", "x": 570, "y": 220, "wires": [ [ "676931a1.872fc" ] ] }, { "id": "3134692e.250326", "type": "inject", "z": "947f7b4d.ee6ef8", "name": "Wind Chimes One", "props": [ { "p": "payload", "v": "wind chimes one", "vt": "str" }, { "p": "topic", "v": "amy", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "topic": "amy", "payload": "wind chimes one", "payloadType": "str", "x": 1011, "y": 301, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "94bb09db.a9bad8", "type": "inject", "z": "947f7b4d.ee6ef8", "name": "100%", "repeat": "", "crontab": "", "once": true, "onceDelay": "1", "topic": "", "payload": "100", "payloadType": "str", "x": 130, "y": 80, "wires": [ [ "4e7da40d.4cef4c" ] ] }, { "id": "4e7da40d.4cef4c", "type": "subflow:c75889b6.c114a8", "z": "947f7b4d.ee6ef8", "x": 300, "y": 80, "wires": [] }, { "id": "14222a13.e6f206", "type": "inject", "z": "947f7b4d.ee6ef8", "name": "thunder", "props": [ { "p": "payload", "v": "thunder", "vt": "str" }, { "p": "topic", "v": "amy", "vt": "string" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": "", "topic": "amy", "payload": "thunder", "payloadType": "str", "x": 970, "y": 60, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "7f84915b.077af", "type": "inject", "z": "947f7b4d.ee6ef8", "name": "", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "amy", "payload": "dogs barking", "payloadType": "str", "x": 1010, "y": 340, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "1ec9a4f724dff8c4", "type": "exec", "z": "947f7b4d.ee6ef8", "command": "sudo mpg123", "addpay": true, "append": "", "useSpawn": "", "timer": "", "oldrc": false, "name": "Mp3 player", "x": 1610, "y": 220, "wires": [ [ "2cc43b1538cd8d17" ], [], [] ] }, { "id": "e8f1df220b9a0e3b", "type": "function", "z": "947f7b4d.ee6ef8", "name": "Clr creating", "func": "global.set(\"create_speech_busy\",0);\nmsg.payload=\"\";\nreturn msg;", "outputs": 1, "noerr": 0, "x": 1450, "y": 80, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "c6b58278606d97a5", "type": "function", "z": "947f7b4d.ee6ef8", "name": "process text", "func": "if (typeof context.arr == \"undefined\" || !(context.arr instanceof Array)) context.arr = [];\nif (typeof global.get(\"speech_busy\") == \"undefined\") global.set(\"speech_busy\", 0);\nif (typeof global.get(\"create_speech_busy\") == \"undefined\") global.set(\"create_speech_busy\", 0);\n\nif (msg.payload !== \"\") context.arr.push(msg.payload);\nif (context.arr.length) {\n msg.payload = context.arr.shift();\n if (msg.payload.indexOf(\".mp3\") == -1) {\n var fs = global.get(\"fs\");\n var mess = msg.payload;\n mess = mess.replace(/'/g, \"\");\n var messfile = mess.toLowerCase();\n messfile = messfile.replace(/[.,\\/#!$%\\^&\\*;:{}=\\-_`~()]/g, \"\");\n messfile = messfile.replace(/ /g, \"_\");\n messfile = \"/home/pi/audio/\" + messfile + \".mp3\";\n\n if (fs.existsSync(messfile)) {\n if (global.get(\"speech_busy\")==1) { context.arr.unshift(msg.payload); return [null, null]; }\n else { global.set(\"speech_busy\", 1); msg.payload = messfile; return [null, msg]; }\n }\n else {\n if (global.get(\"create_speech_busy\")==1) { context.arr.unshift(msg.payload); return [null, null]; } else\n {\n context.arr.unshift(msg.payload);\n global.set(\"create_speech_busy\",1);\n var voice = \"Amy\";\n msg.payload = 'polly synthesize-speech --engine \"neural\" --region eu-west-2 --output-format mp3 --voice-id ' + voice + ' --text \"' + mess + '\" ' + messfile;\n return [msg, null];\n }\n }\n }\n if (global.get(\"speech_busy\")==1) context.arr.unshift(msg.payload); \n else { global.set(\"speech_busy\", 1); return [null, msg]; } // mp3 or synth \n}\n", "outputs": "2", "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1430, "y": 180, "wires": [ [ "f26c3a4e0246fb10" ], [ "1ec9a4f724dff8c4" ] ] }, { "id": "2cc43b1538cd8d17", "type": "function", "z": "947f7b4d.ee6ef8", "name": "Clr playing", "func": "global.set(\"speech_busy\",0);\nmsg.payload=\"\"; return msg;", "outputs": "1", "noerr": 0, "x": 1450, "y": 300, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "f26c3a4e0246fb10", "type": "exec", "z": "947f7b4d.ee6ef8", "command": "aws", "addpay": "payload", "append": "", "useSpawn": "false", "timer": "", "winHide": false, "name": "aws create", "x": 1610, "y": 160, "wires": [ [ "e8f1df220b9a0e3b" ], [], [] ] }, { "id": "af813dae06f48d2e", "type": "inject", "z": "947f7b4d.ee6ef8", "name": "Power up clear speech busy", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": true, "onceDelay": 0.1, "topic": "", "payload": "", "payloadType": "date", "x": 200, "y": 40, "wires": [ [ "dc4cc669d013eb9f" ] ] }, { "id": "dc4cc669d013eb9f", "type": "function", "z": "947f7b4d.ee6ef8", "name": "Clr creating", "func": "global.set(\"create_speech_busy\",0);\nglobal.set(\"speech_busy\",0);", "outputs": 0, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 450, "y": 40, "wires": [] }, { "id": "a46305c0988fdf5e", "type": "inject", "z": "947f7b4d.ee6ef8", "name": "new", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "amy", "payload": "new speech", "payloadType": "str", "x": 1250, "y": 340, "wires": [ [ "c6b58278606d97a5" ] ] } ] There’s a piece missing in the above – the volume which I have as a NR sub-flow (just make a normal flow with INJECT nodes for testing).My volume sub-flow relies on amixer: I feed a value 0-100 to a function (yellow box above) which then passes it’s output onto an EXEC function (orange).Sound values for amixer here 0-100 for 0% to 100% – the output sound levels are not linear hence a simple fix below: var inv=msg.payload; if (inv<5) inv=0; else if (inv<10) inv=50; else if (inv<20) inv=67; else if (inv<30) inv=77; else if (inv<40) inv=84; else if (inv<50) inv=89; else if (inv<60) inv=93; else if (inv<70) inv=96; else if (inv<80) inv=98; else if (inv<90) inv=99; else inv=100; msg.payload="amixer cset numid=1 -- " + inv + "%"; return msg; The content of the NR Exec node for volume control (again for Pi OS before Bookworm): OK, HOLD THE PHONE – I just noticed a comment from 2017 referring to a node-red-node called node-red-contrib-polly-tts so this morning in 2024 I went off looking for it – and installed it.

Sure enough it WORKS – BUT… simply feed text to it and your AWS credentials, add a simple function one-liner to convert msg.file to msg.payload and feed it to the MP3 Player exec node described earlier – for a second I was worried but when I compared my node set with this – result: Amy sound utterly BORED – I don’t know how to better describe it and I don’t THINK I can easily include audio files in the blog to demonstrate.This POLY-TTS node referred to above definitely works and buffers messages but it also does NOT queue them “How are y..how are you doing” for example with the first message truncated..

and the speech is very matter-of-fact synth – not a human asking a genuine question – so I’m not out of business yet And Now, RPi4 and RPi5 Node-Red – Pi OS Bookworm – Docker version Elswhere I’ve covered installing Antionio Fragola’s Docker setup on the RPi4 and 5 using P1 OS 64-bit Bookworm as there are charges in the setup which means my popular “The Script” setup script is no longer of practical use in the new environment – So Antonio took on the task of creating a Docker setup with all the utilities I normally use – Node-Red, Mosquitto, Grafana and much more – in a more device-independent manner.I’ve adopted this and Docker indeed does make for easy setup, backups and updates – but I’m sticking with my favourite SBC – Raspberry Pi mainly because of experience and rpi-clone – read about all of that in the related blog entries – but here I’m covering my speech setup on the Pi: amixer and mpg123 are running natively on the RPI4 as before I’m now including RPi5.Node-Red is now running in a Docker container and the aws-cli is also running in the Docker container – rather than copy information which could quickily become out of date, for installation of Node-Red and optionally much more.

in order to then implement this audio setup on the RPi4/ RPi5 on which you will have set up Bookwork or Bookwork Lite, see Antonio’s GIT repository DockerIOT.Firstly you might want a supply of .mp3 files – in this case mine (for testing – not my real collection) Referred to in here are a mix of pre-recorded .mp3 files – Star Trek alerts etc.and grabbed speech from aws Polly.

See the video for details on the Node-Red code and example use… (slight reverb in the audio – but simple short videos never work as as they are supposed to).[ { "id": "d889a35bfcc63c2f", "type": "exec", "z": "98fc211d841d9a2d", "command": "ssh host", "addpay": "payload", "append": "", "useSpawn": "false", "timer": "", "winHide": false, "oldrc": false, "name": "mpg123 on host", "x": 1340, "y": 340, "wires": [ [ "2cc43b1538cd8d17" ], [], [] ] }, { "id": "a51be2c7bd5a93c4", "type": "exec", "z": "98fc211d841d9a2d", "command": "ssh host", "addpay": "payload", "append": "", "useSpawn": "false", "timer": "", "winHide": false, "oldrc": false, "name": "aws in container exec node", "x": 1380, "y": 180, "wires": [ [ "e8f1df220b9a0e3b" ], [], [] ] }, { "id": "1738bb65.3471d5", "type": "inject", "z": "98fc211d841d9a2d", "name": "50%", "repeat": "", "crontab": "", "once": false, "topic": "", "payload": "50", "payloadType": "str", "x": 390, "y": 180, "wires": [ [ "0497724a81f21b0e" ] ] }, { "id": "10e04ec3.32a121", "type": "inject", "z": "98fc211d841d9a2d", "name": "25%", "repeat": "", "crontab": "", "once": false, "topic": "", "payload": "25", "payloadType": "str", "x": 390, "y": 140, "wires": [ [ "0497724a81f21b0e" ] ] }, { "id": "8f283f57.3b399", "type": "inject", "z": "98fc211d841d9a2d", "name": "Red Alert", "props": [ { "p": "payload", "v": "red alert", "vt": "str" }, { "p": "topic", "v": "amy", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "topic": "amy", "payload": "red alert", "payloadType": "str", "x": 760, "y": 220, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "2b461c2.7bd40e4", "type": "inject", "z": "98fc211d841d9a2d", "name": "Comms beep", "props": [ { "p": "payload", "v": "comms beep", "vt": "str" }, { "p": "topic", "v": "amy", "vt": "string" } ], "repeat": "", "crontab": "", "once": false, "topic": "amy", "payload": "comms beep", "payloadType": "str", "x": 770, "y": 260, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "e928e8.25ad7718", "type": "inject", "z": "98fc211d841d9a2d", "name": "Hailing frequencies open", "repeat": "", "crontab": "", "once": false, "topic": "amy", "payload": "hailing frequencies open", "payloadType": "str", "x": 810, "y": 300, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "2d98b3aa.d7284c", "type": "inject", "z": "98fc211d841d9a2d", "name": "Keypress", "props": [ { "p": "payload", "v": "keypress", "vt": "str" }, { "p": "topic", "v": "amy", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "topic": "amy", "payload": "keypress", "payloadType": "str", "x": 760, "y": 340, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "b0d13aff.e60fd8", "type": "inject", "z": "98fc211d841d9a2d", "name": "Unable to comply", "props": [ { "p": "payload", "v": "unable to comply", "vt": "str" }, { "p": "topic", "v": "amy", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "topic": "amy", "payload": "unable to comply", "payloadType": "str", "x": 780, "y": 380, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "7e2cec04.b03ee4", "type": "inject", "z": "98fc211d841d9a2d", "name": "0%", "repeat": "", "crontab": "", "once": false, "topic": "", "payload": "0", "payloadType": "str", "x": 390, "y": 100, "wires": [ [ "0497724a81f21b0e" ] ] }, { "id": "e4d915f1.5aaea8", "type": "inject", "z": "98fc211d841d9a2d", "name": "100%", "repeat": "", "crontab": "", "once": false, "topic": "", "payload": "100", "payloadType": "str", "x": 390, "y": 260, "wires": [ [ "0497724a81f21b0e" ] ] }, { "id": "b98733e4.20171", "type": "inject", "z": "98fc211d841d9a2d", "name": "75%", "repeat": "", "crontab": "", "once": false, "topic": "", "payload": "75", "payloadType": "str", "x": 390, "y": 220, "wires": [ [ "0497724a81f21b0e" ] ] }, { "id": "3134692e.250326", "type": "inject", "z": "98fc211d841d9a2d", "name": "Wind Chimes One", "props": [ { "p": "payload", "v": "wind chimes one", "vt": "str" }, { "p": "topic", "v": "amy", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "topic": "amy", "payload": "wind chimes one", "payloadType": "str", "x": 790, "y": 420, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "94bb09db.a9bad8", "type": "inject", "z": "98fc211d841d9a2d", "name": "Powerup 100%", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": true, "onceDelay": "1", "topic": "", "payload": "100", "payloadType": "str", "x": 420, "y": 40, "wires": [ [ "0497724a81f21b0e" ] ] }, { "id": "14222a13.e6f206", "type": "inject", "z": "98fc211d841d9a2d", "name": "thunder", "props": [ { "p": "payload", "v": "thunder", "vt": "str" }, { "p": "topic", "v": "amy", "vt": "string" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": "", "topic": "amy", "payload": "thunder", "payloadType": "str", "x": 750, "y": 180, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "7f84915b.077af", "type": "inject", "z": "98fc211d841d9a2d", "name": "", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "amy", "payload": "dogs barking", "payloadType": "str", "x": 790, "y": 460, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "e8f1df220b9a0e3b", "type": "function", "z": "98fc211d841d9a2d", "name": "Clr creating", "func": "global.set(\"create_speech_busy\",0);\nmsg.payload=\"\";\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1330, "y": 280, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "c6b58278606d97a5", "type": "function", "z": "98fc211d841d9a2d", "name": "process text", "func": "if (typeof context.arr == \"undefined\" || !(context.arr instanceof Array)) context.arr = [];\nif (typeof global.get(\"speech_busy\") == \"undefined\") global.set(\"speech_busy\", 0);\nif (typeof global.get(\"create_speech_busy\") == \"undefined\") global.set(\"create_speech_busy\", 0);\n\nif (msg.payload !== \"\") context.arr.push(msg.payload);\nif (context.arr.length) {\n msg.payload = context.arr.shift();\n if (msg.payload.indexOf(\".mp3\") == -1) {\n var fs = global.get(\"fs\");\n var mess = msg.payload;\n mess = mess.replace(/'/g, \"\");\n var messfile = mess.toLowerCase();\n messfile = messfile.replace(/[.,\\/#!$%\\^&\\*;:{}=\\-_`~()]/g, \"\");\n messfile = messfile.replace(/ /g, \"_\");\n messfile = \"/data/audio/\" + messfile + \".mp3\";\n\n if (fs.existsSync(messfile)) {\n if (global.get(\"speech_busy\")==1) { context.arr.unshift(msg.payload); return [null, null]; }\n else { global.set(\"speech_busy\", 1); msg.payload = \"mpg123 /root/DockerIOT/nodered\" + messfile; return [null, msg]; }\n }\n else {\n if (global.get(\"create_speech_busy\")==1) { context.arr.unshift(msg.payload); return [null, null]; } else\n {\n context.arr.unshift(msg.payload);\n global.set(\"create_speech_busy\",1);\n var voice = \"Amy\";\n // msg.payload = 'polly synthesize-speech --engine \"neural\" --region eu-west-2 --output-format mp3 --voice-id ' + voice + ' --text \"' + mess + '\" ' + messfile;\n \n msg.payload = \"docker run --rm -i -v /root:/root amazon/aws-cli \\\"polly synthesize-speech --output-format mp3 --engine 'neural' --voice-id \" + voice + \" --region eu-west-2 --text '\" + mess + \"' /root/DockerIOT/nodered\" + messfile + \"\\\"\";\n return [msg, null];\n }\n }\n }\n if (global.get(\"speech_busy\")==1) context.arr.unshift(msg.payload); \n else { global.set(\"speech_busy\", 1); return [null, msg]; } // mp3 or synth \n}\n", "outputs": "2", "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1130, "y": 220, "wires": [ [ "a51be2c7bd5a93c4" ], [ "d889a35bfcc63c2f" ] ] }, { "id": "2cc43b1538cd8d17", "type": "function", "z": "98fc211d841d9a2d", "name": "Clr playing", "func": "global.set(\"speech_busy\",0);\nmsg.payload=\"\"; return msg;", "outputs": "1", "noerr": 0, "x": 1330, "y": 440, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "af813dae06f48d2e", "type": "inject", "z": "98fc211d841d9a2d", "name": "Power up clear speech busy", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": true, "onceDelay": 0.1, "topic": "", "payload": "", "payloadType": "date", "x": 880, "y": 140, "wires": [ [ "dc4cc669d013eb9f" ] ] }, { "id": "dc4cc669d013eb9f", "type": "function", "z": "98fc211d841d9a2d", "name": "Clear both busy flags", "func": "global.set(\"create_speech_busy\",0);\nglobal.set(\"speech_busy\",0);", "outputs": 0, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1120, "y": 140, "wires": [] }, { "id": "a46305c0988fdf5e", "type": "inject", "z": "98fc211d841d9a2d", "name": "Have a nice day!", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "amy", "payload": "Have a nice day!", "payloadType": "str", "x": 1060, "y": 380, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "0497724a81f21b0e", "type": "function", "z": "98fc211d841d9a2d", "name": "weird volume relationship 0% to 100%", "func": "var inv=msg.payload;\nif (inv<5) inv=0;\nelse if (inv<10) inv=50;\nelse if (inv<20) inv=67;\nelse if (inv<30) inv=77;\nelse if (inv<40) inv=84;\nelse if (inv<50) inv=89;\nelse if (inv<60) inv=93;\nelse if (inv<70) inv=96;\nelse if (inv<80) inv=98;\nelse if (inv<90) inv=99;\nelse inv=100;\n\nmsg.payload=\"amixer cset numid=1 -- \" + inv + \"%\";\nreturn msg;\n", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 890, "y": 100, "wires": [ [ "d797dff4a2ca6b82" ] ] }, { "id": "d797dff4a2ca6b82", "type": "exec", "z": "98fc211d841d9a2d", "command": "ssh host", "addpay": "payload", "append": "", "useSpawn": "false", "timer": "", "winHide": false, "name": "amixer volume on host", "x": 1360, "y": 100, "wires": [ [], [], [] ] }, { "id": "8d0620f76ad8f155", "type": "inject", "z": "98fc211d841d9a2d", "name": "enter authorization code", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "amy", "payload": "enter authorization code", "payloadType": "str", "x": 800, "y": 500, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "10e8662aeb1ad2b2", "type": "inject", "z": "98fc211d841d9a2d", "name": "Are you having a nice day?", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "amy", "payload": "Are you having a nice day?", "payloadType": "str", "x": 1090, "y": 440, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "068a9af05fb550e2", "type": "inject", "z": "98fc211d841d9a2d", "name": "working", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "amy", "payload": "working", "payloadType": "str", "x": 750, "y": 540, "wires": [ [ "c6b58278606d97a5" ] ] }, { "id": "08210bd0c47a291d", "type": "inject", "z": "98fc211d841d9a2d", "name": "Are you having a nice day, hmm?", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "amy", "payload": "Are you having a nice day, hmm?", "payloadType": "str", "x": 1110, "y": 500, "wires": [ [ "c6b58278606d97a5" ] ] } ] This is important – assuming Docker is installed following Antonio’s method with /root/DockerIOT as the base – and Node-Red is already set up – you will then create the audio folder as below and you will store your files in there.To Node-Red, they will LOOK as if they are simply stored in /data/audio/ folder because Docker puts applications in their own effectively isolated environment.

mkdir -p /root/DockerIOT/nodered/data/audio chown -R 1000:1000 /root/DockerIOT/nodered/data The key difference between aws (not Polly which is just one of many aws plugins) and the other containers is that aws runs as a command, it is called and after it completes (good or bad, not important now) its job, it exits and does not leave anything running in the background… Once Node-Red is up and running in Docker, use Antonio’s alias at the command line in /root/DockerIOT/nodered and simply enter dstop (or the hard way: docker compose down) to stop Node-Red temporarily.Then go into the Node-Red folder – settings.js file and carefully add at the very beginning: var fs = require("fs"); and at the end of settings.js, INSIDE the functionGlobalContext section add one line: functionGlobalContext: { fs:require('fs'), }, That applies to both the RPi4 and RPi5 versions.Note: Antonio recommends factorizing that “/root/DockerIOT/nodered” which is used a couple of times in that flow, in a variable, let’s say, “host_path_prefix”, to clarify further that should be added ONLY for commands running on the host (via the ssh exec node) that need the full path instead of just the internal subpaths “/data/…” which instead should be used for nodes accessing stuff INSIDE the container itself… BOTH point to the SAME data (thanks to the volume mounting the local folder to the container one) but should be managed differently: commands running on the host via ssh and addressing HOST should use full paths INCLUDING that “prefix” one, while commands addressing CONTAINER mounted paths and because of this with normal nodes, and not exec and ssh, won’t need that prefix.

I’ve been playing at root command level on the RPi5: aws polly synthesize-speech --output-format mp3 --engine 'neural' --voice-id Amy --region eu-west-2 --text "Hmmm..that's very nice!" /root/DockerIOT/nodered/data/audio/nonsense.mp3 mpg123 /root/DockerIOT/nodered/data/audio/nonsense.mp3 That’s working – but amixer was being awkward: amixer cset numid=1 -- 100% The latter was failing… HOWEVER, the MP3 file plays and ALSAMIXER (pre-installed – NOT command line – some kind of CLI graphical interface) lets me set the volume higher than I’d managed so far on RPi5.Hitting F6 says the audio device is default 0: USB Audio Device – yet amixer on RPi5 says that device 0 is an invalid device.

Amixer on RPi5 however DOES seem happy with numid=6 So, by experimenting, I discovered that on the RPi5 both in Docker and in native mode, I had to rethink the volume control – and by experiment, with value 400 (not percent) as full and value 0 as apparently 0, I came up with a non-linear solution for my volume control – here’s the “entire “non linear” function revamped for RPi5 only.Feel free to play with those intermediate values.var inv=msg.payload; if (inv==0) inv=0; else if (inv<=25) inv=10; else if (inv<=50) inv=20; else if (inv<=75) inv=25; else if (inv<=100) inv=400; msg.payload="amixer cset numid=6 -- " + inv; return msg; And in the end… With apologies to those who have commented on the AWS Polly article – now hidden – I tried to introduce a better WordPress plug-in for showing large code sections (color-coding, summary views) but it ended up killing the original version of this blog entry – if you see any new comments in here – be very careful if copying code from them – WordPress has a habit of mangling quotes and other special characters.

Given the above recent experience I’m wary of experimenting with other (for me) untested plug-ins which might improve the native comments code or indeed improve on the existing code blocks.

Read More
Related Posts