Amazon AWS Polly Neural Speech on RPi4 and RPi5 - Scargill's Tech Blog

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.Well, Ivona, good as it was, has been defunct now for some time.

AWS Polly is great, especially with the “neural” enhancement – and I’m now using it on RPi4 in Spain with a view to running on RPi5 soon.Read through to the end for updates… This post was originally written for Ivona in 2017 and was then completely overhauled for the (then new new) option in Polly as well as (optionally) including the voice-id in the input.Update April 2024 – the neural option MUST be accompanied by a region option (I don’t live in USA WEST-anything – but this is one of a few regions you can enter for it to work.

The Amazon Polly system is effectively a replacement for Ivona.The short, sharp answer is: Polly works, it is effectively free (<5 million characters a month or 1 million characters with the new “neural” option) – and it is better than Ivona.It is one of many Amazon AWS services… so CLI access usually begins “aws polly …” Read on, as my simple Node-Red code caches data 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 this image from a free-use, no-attribute site So the Amazon system “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 seems appropriate  – my code downloads a phrase as a file (MP3) from your 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, plays it, if not, gets a new file from AWS Polly and plays that.In a typical use-case that I might have, after a message is used once, it is kept in it’s own file for re-use and hence 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.We’ve just got this to work on Node-Red in the latest PI OS (Bookworm 64 bit) in a Docker container on RPi4.RPi5 soon.

I’ll assume you have your aws credentials (ID and secret – simple – free) – don’t worry about location not being where you actually are.To make sure AWS is working, use the command-line (CLI) code below – I’ve used this on RPi2-4 without any issues.Currently using it on PI OS (Bullseye 32 bit).

As user pi, I created a folder called /home/pi/audio to store files… then… oh, another update – 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 I set the region to us-west-1 in the original setup 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 absolutely works 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 seems to override some initial settings.Once AWS was installed, I used: aws configure to enter the user ID and secret key (both of which I’d already set up on the Amazon site – for once – easy), location and format.

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 at the command line (if you see options starting in a single dash – WordPress has messed them up – they all start with double dashes – see image: 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 Amixer – well, on RPi 5 amixer SEEMS to operate very differently so I’ve had to change 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., see diagrams – but at last – it WORKS.i.e.## max volume on RPi4 with internal audio amixer cset numid=1 -- 100% Next step…  mpg123 /home/pi/audio/peter.mp3 Sorted – good for testing but as you’ll see the final solution is much better.

I have this the wrong way around of course – you should first ensure you have a working MPG123 player with some standard .MP3 file before testing aws Polly – to keep life simple.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 – I’ll go into that later in the article.If you want to add sound effects – just put .MP3 files in the audio folder and call them by filename minus the extension.I have files like red-alert.mp3 and similar using Star Trek recordings – far better to have the original than a modern voice wailing “red alert”? The first function looks to see if the payload has something in it and if so it pushes that onto a stack.

The code then looks to see if speech is busy – if not and if there is something on the stack, it checks – if it is an .mp3 file it sends the file to the MP3 player.If it is not an mp3, it looks to see if you’ve already created an .mp3 for that speech, if so it plays that file, otherwise it passes the message onto Amazon to create the file – which is then played back with a small delay depending on your broadband speed.It would have been nice to process new speech while playing something else back but that would get more complicated, involving more flags.

As it stands this is easy to understand.You can fire in more speech or .MP3 files while one is playing and they will simply be queued.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 in my standard “The Script” – useful for earlier versions of Pi OS.Note – when it comes to RPi5 – there is no 3.5mm jack output so it is necessary to use a USB dongle ..and also calling aws and mpg123 from a Docker container also needs some thought as Typically AWS will be called directly from The Node-Red container whereas MPG123 and the sound mixer (volume control AMIXER) will be native.

Read on.Here is the code I used in each of those functions….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 three yellow function nodes below: I 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 sub-flow (doesn’t have to be).

So the volume sub-flow relies on amixer: So we feed a value 0-100 to a function which then passes that onto an exec function.Node-Red volume control function node: (2024) I just changed it as the default sound mixer has really weird volume settings: 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; Content of the NR Exec node for volume control (again for Pi OS before Bookworm): April 28, 2024 – 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.

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.This POLY-TTS node 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 In fact, I’ve now moved to Node-Red in Docker on both RPi4 and 5 and below I’ll show you the Node-Red-In-Docker version on the Rpi4 – RPi5 to follow when we get audio running on the latter.End of native Node-Red version RPi4 Node-Red in Docker version Essentially the same but amixer and mpg123 are running natively on the RPI4 as before whereas Node-Red is running in a Docker container and the aws-cli is also running in the Docker container – if you need more information to implement this on the RPi4 and Bookwork/Bookwork Lite, see Antonio’s GIT repository DockerIOT.

Firstly a supply of .mp3 files – in this case mine (for testing – not my real collection) copied in here are nearly all pre-recorded .mp3 files – Star Trek alerts etc.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).The entire flow: And the flow code below (I wish I could reduce this to a heading) until the user presses a button.

This should be copied into new Node Red flow page – and ONLY the IP address in the exec nodes will need changing – be VERY careful.I’m assuming here that the user has adopted Antonio’s DockerIOT folder structure – it will also be necessary to down the NR container – and add in the data/audio CHOWN – the ACTUAL folder being as shown in the video i.e./root/DockerIOT/nodered/data/audio – clearly the user will not have my already-recorded mp3 files.

[ { "id": "d889a35bfcc63c2f", "type": "exec", "z": "98fc211d841d9a2d", "command": "ssh -x -C -i /usr/src/node-red/.ssh/id_nr -o \"StrictHostKeyChecking=accept-new\" -p 22 [email protected]", "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 -x -C -i /usr/src/node-red/.ssh/id_nr -o \"StrictHostKeyChecking=accept-new\" -p 22 [email protected]", "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 -i /usr/src/node-red/.ssh/id_nr -o \"StrictHostKeyChecking=accept-new\" -p 22 [email protected]", "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: mkdir -p /root/DockerIOT/nodered/data/audio chown -R 1000:1000 /root/DockerIOT/nodered/data Clarification as I likely missed this above: The key difference between aws (not polly, polly is just one of its 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… To run AWS in both RPi4 and RPi5, a simple alias in the /root/.bashrc file is needed.alias aws='docker run --rm -it -v /root:/root amazon/aws-cli' All the other containers in Antonio’s repo, are instead put up as system services, so they actually stay and run in the background to answer the user requests… To add EXEC permission on the host… BEWARE, HIGHLY INSECURE if nodered exposed to public and not well protected, the ssh keypair that will be used will allow FULL ROOT ACCESS to the underlying HOST OS!!! Shutdown the nodered container, then edit its docker-compose.yaml, add this to the volumes section (respect indentation, it should be at same level as the other line about “data” folder): - ./ssh:/usr/src/node-red/.ssh Create the folder that will contain the ssh keypair needed for nodered to access the host system without a password: mkdir -p /root/DockerIOT/nodered/ssh chown -R 1000:1000 /root/DockerIOT/nodered/ssh cd /root/DockerIOT/nodered dstart # if not working, apply aliases detailed in Antonio's repo dbash # if not working, apply aliases detailed in Antonio's repo Now you’re INSIDE the nodered docker container… generate an ssh keypair, with no passphrase: ssh-keygen -f /usr/src/node-red/.ssh/id_nr -t ed25519 -q -N "" Copy the public key into the HOST system’s authorized_keys, change the ssh port if needed (default 22) and ip address, and provide the HOST root password when asked: ssh-copy-id -i /usr/src/node-red/.ssh/id_nr -o "StrictHostKeyChecking=accept-new" -p 22 [email protected] Test to ensure that ssh from container to host now works, without any password request, again change the ssh port if needed (default 22) and ip address: ssh -i /usr/src/node-red/.ssh/id_nr -o "StrictHostKeyChecking=accept-new" -p 22 [email protected] Type exit to close the ssh session, exit again to close the shell inside the container.Node-Red – my code requires access to the file system, so at the VERY START of settings.js BEFORE the module.exports section I add: var fs = require("fs"); and at the end INSIDE the functionGlobalContext section I add: functionGlobalContext: { fs:require('fs'), // os:require('os'), }, That applies to both the RPi4 and Rpi5 versions.

The other thing: 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 funny: 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 says 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 can up with a non-linear solution for my volume control – here’s the “entire “non linear” function revamped for RPi5… 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;

Read More
Related Posts