Nodered Text-to-Speech in Windows

Hi there guys,

I'm trying to setup a simple text-to-speech function in Windows using powershell, but i'm somehow failing miserably.

I've managed to create a flow that works:

[
    {
        "id": "8fc0ac7099b7175f",
        "type": "change",
        "z": "73264e222aeaf471",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "\"powershell -NoProfile -command \\\"Add-Type -AssemblyName System.speech; $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; $speak.SpeakSsml(\" & $.payload & \")\\\"\"",
                "tot": "jsonata"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 520,
        "y": 320,
        "wires": [
            [
                "d0814fabfbee02ef"
            ]
        ]
    },
    {
        "id": "d0814fabfbee02ef",
        "type": "exec",
        "z": "73264e222aeaf471",
        "command": "",
        "addpay": "payload",
        "append": "",
        "useSpawn": "",
        "timer": "",
        "winHide": false,
        "name": "TTS",
        "x": 750,
        "y": 320,
        "wires": [
            [
                "ef75f9ba76381361"
            ],
            [
                "ef75f9ba76381361"
            ],
            [
                "ef75f9ba76381361"
            ]
        ]
    },
    {
        "id": "ef75f9ba76381361",
        "type": "debug",
        "z": "73264e222aeaf471",
        "name": "",
        "active": true,
        "console": "false",
        "complete": "false",
        "x": 950,
        "y": 320,
        "wires": []
    },
    {
        "id": "84d2cfb3f19d50b9",
        "type": "inject",
        "z": "73264e222aeaf471",
        "name": "",
        "props": [
            {
                "p": "payload",
                "v": "",
                "vt": "str"
            },
            {
                "p": "topic",
                "v": "",
                "vt": "string"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "str",
        "x": 170,
        "y": 320,
        "wires": [
            [
                "ca096792549bd376"
            ]
        ]
    },
    {
        "id": "ca096792549bd376",
        "type": "template",
        "z": "73264e222aeaf471",
        "name": "set payload",
        "field": "payload",
        "fieldType": "msg",
        "format": "handlebars",
        "syntax": "plain",
        "template": "'<speak version=\\\"1.0\\\" xmlns=\\\"http://www.w3.org/2001/10/synthesis\\\" xml:lang=\\\"pt-BR\\\"><voice name=\\\"pt-BR-FranciscaNeural\\\"><prosody rate=\\\"1\\\"><p><prosody pitch=\\\"x-high\\\"> Testing TTS </prosody></p></prosody></voice></speak>'",
        "output": "str",
        "x": 330,
        "y": 320,
        "wires": [
            [
                "8fc0ac7099b7175f"
            ]
        ]
    }
]

But this has the message built inside the payload. I want to inject the message from another node, like an inject node, for example, but then the command fails:

[
    {
        "id": "ac2b24e822dd8318",
        "type": "exec",
        "z": "73264e222aeaf471",
        "command": "",
        "addpay": "payload",
        "append": "",
        "useSpawn": "",
        "timer": "",
        "winHide": false,
        "name": "TTS",
        "x": 750,
        "y": 440,
        "wires": [
            [
                "d22a6033c7d23a0f"
            ],
            [
                "d22a6033c7d23a0f"
            ],
            [
                "d22a6033c7d23a0f"
            ]
        ]
    },
    {
        "id": "d22a6033c7d23a0f",
        "type": "debug",
        "z": "73264e222aeaf471",
        "name": "",
        "active": true,
        "console": "false",
        "complete": "false",
        "x": 930,
        "y": 440,
        "wires": []
    },
    {
        "id": "ef038e214b8a2065",
        "type": "change",
        "z": "73264e222aeaf471",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "\"'<speak version=\\\"1.0\\\" xmlns=\\\"http://www.w3.org/2001/10/synthesis\\\" xml:lang=\\\"pt-BR\\\"><voice name=\\\"pt-BR-FranciscaNeural\\\"><prosody rate=\\\"1\\\"><p><prosody pitch=\\\"x-high\\\"> \" & $.payload & \"</prosody></p></prosody></voice></speak>'\"",
                "tot": "jsonata"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 380,
        "y": 440,
        "wires": [
            [
                "ab199d7bb8d6aec8"
            ]
        ]
    },
    {
        "id": "7cf28b0fe0bda564",
        "type": "inject",
        "z": "73264e222aeaf471",
        "name": "",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "Testing TTS",
        "payloadType": "str",
        "x": 190,
        "y": 440,
        "wires": [
            [
                "ef038e214b8a2065"
            ]
        ]
    },
    {
        "id": "ab199d7bb8d6aec8",
        "type": "change",
        "z": "73264e222aeaf471",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "\"powershell -NoProfile -command \\\"Add-Type -AssemblyName System.speech; $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; $speak.SpeakSsml(\" & $.payload & \")\\\"\"",
                "tot": "jsonata"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 580,
        "y": 440,
        "wires": [
            [
                "ac2b24e822dd8318"
            ]
        ]
    }
]

When executing this flow, it shows in the debug panel:

Exception calling "SpeakSsml" with "1" argument(s): "XML content not valid."
At line:1 char:102
+ ... ynthesizer; $speak.SpeakSsml('<speak version=1.0 xmlns=http://www.w3. ...
+                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : FormatException

and also:

Command failed:  powershell -NoProfile -command "Add-Type -AssemblyName System.speech; $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; $speak.SpeakSsml('<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="pt-BR"><voice name="pt-BR-FranciscaNeural"><prosody rate="1"><p><prosody pitch="x-high"> Testing TTS</prosody></p></prosody></voice></speak>')"
Exception calling "SpeakSsml" with "1" argument(s): "XML content not valid."
At line:1 char:102
+ ... ynthesizer; $speak.SpeakSsml('<speak version=1.0 xmlns=http://www.w3. ...
+                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : FormatException

What am i doing wrong?

I am not at my PC at the moment, but the first thing to do is to add a debug node showing what is going into the node in each case. There must be a difference. If you can't see it then post the two messages here.

Hi Colin,

Both nodes have a debug log. The working one doesn't show any output, but the one that's not working shows the errors i've shown in the first post.

In that case it isn't showing what is going into the exec node.

This works:

[
    {
        "id": "8fc0ac7099b7175f",
        "type": "change",
        "z": "73264e222aeaf471",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "\"powershell -NoProfile -command \\\"Add-Type -AssemblyName System.speech; $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; $speak.SelectVoice('Microsoft Daniel'); $speak.SpeakSsml(\" & $.payload & \")\\\"\"",
                "tot": "jsonata"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 520,
        "y": 320,
        "wires": [
            [
                "d0814fabfbee02ef"
            ]
        ]
    },
    {
        "id": "d0814fabfbee02ef",
        "type": "exec",
        "z": "73264e222aeaf471",
        "command": "",
        "addpay": "payload",
        "append": "",
        "useSpawn": "false",
        "timer": "",
        "winHide": false,
        "name": "TTS",
        "x": 750,
        "y": 320,
        "wires": [
            [
                "ef75f9ba76381361"
            ],
            [
                "ef75f9ba76381361"
            ],
            [
                "ef75f9ba76381361"
            ]
        ]
    },
    {
        "id": "ef75f9ba76381361",
        "type": "debug",
        "z": "73264e222aeaf471",
        "name": "",
        "active": true,
        "console": "false",
        "complete": "false",
        "x": 950,
        "y": 320,
        "wires": []
    },
    {
        "id": "84d2cfb3f19d50b9",
        "type": "inject",
        "z": "73264e222aeaf471",
        "name": "",
        "props": [
            {
                "p": "payload",
                "v": "",
                "vt": "str"
            },
            {
                "p": "topic",
                "v": "",
                "vt": "string"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "str",
        "x": 170,
        "y": 320,
        "wires": [
            [
                "ca096792549bd376"
            ]
        ]
    },
    {
        "id": "ca096792549bd376",
        "type": "template",
        "z": "73264e222aeaf471",
        "name": "set payload",
        "field": "payload",
        "fieldType": "msg",
        "format": "handlebars",
        "syntax": "plain",
        "template": "'<speak version=\\\"1.0\\\" xmlns=\\\"http://www.w3.org/2001/10/synthesis\\\" xml:lang=\\\"pt-BR\\\"><voice name=\\\"pt-BR\\\"><prosody rate=\\\"1\\\"><p><prosody pitch=\\\"x-high\\\"> Testing TTS </prosody></p></prosody></voice></speak>'",
        "output": "str",
        "x": 330,
        "y": 320,
        "wires": [
            [
                "8fc0ac7099b7175f"
            ]
        ]
    }
]

But when i click on the inject node, it only shows this:

Screenshot 2021-09-22 121712

Do you not understand what I mean when I say connect a debug node showing what is going into the exec node? I mean this

Do that for the working and non-working and see what the difference is.

By the way, using JSONata just to insert a payload into a string (if that is what you are doing) is horribly inefficient. Use a Template node instead.

I just use the ui_audio node and it works with the browser's TTS support

image

[{"id":"4a9cfcb35995b223","type":"inject","z":"e152a286a73e145c","g":"fe95e26d99960854","name":"Play","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":470,"y":200,"wires":[["bd2b5049605f09d1"]]},{"id":"bd2b5049605f09d1","type":"ui_button","z":"e152a286a73e145c","g":"fe95e26d99960854","name":"","group":"a7ee5c63.381f8","order":1,"width":0,"height":0,"passthru":true,"label":"OK, OK, I am speaking","tooltip":"","color":"","bgcolor":"","icon":"","payload":"\"OK, OK, I am speaking\"","payloadType":"str","topic":"topic","topicType":"msg","x":540,"y":240,"wires":[["f2c3327bec1fb7d4"]]},{"id":"545c506dc2fe1bcd","type":"inject","z":"e152a286a73e145c","g":"fe95e26d99960854","name":"Reset","props":[{"p":"reset","v":"true","vt":"bool"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":470,"y":280,"wires":[["f2c3327bec1fb7d4"]]},{"id":"f2c3327bec1fb7d4","type":"ui_audio","z":"e152a286a73e145c","g":"fe95e26d99960854","name":"","group":"a7ee5c63.381f8","voice":"Google UK English Female","always":true,"x":500,"y":320,"wires":[]},{"id":"a7ee5c63.381f8","type":"ui_group","name":"Speech","tab":"bb93b1da.ab821","order":2,"disp":true,"width":"6","collapse":false},{"id":"bb93b1da.ab821","type":"ui_tab","name":"Tests","icon":"build","order":11,"disabled":false,"hidden":false}]

this is a browser independent setup...it must always work, not only when a browser is open

sorry about that, i really didn't understand before...
here is the output from the working node:

powershell -NoProfile -command "Add-Type -AssemblyName System.speech; $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; $speak.SelectVoice('Microsoft Daniel'); $speak.SpeakSsml('<speak version=\"1.0\" xmlns=\"http://www.w3.org/2001/10/synthesis\" xml:lang=\"pt-BR\"><voice name=\"pt-BR\"><prosody rate=\"1\"><p><prosody pitch=\"x-high\"> Testing TTS </prosody></p></prosody></voice></speak>')"

and here from the non-working node (it differs because the other part of the command is in the exec node, but the xml inside is supposed to be the same):

<speak xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="http://www.w3.org/2001/mstts" xmlns:emo="http://www.w3.org/2009/10/emotionml" version="1.0" xml:lang="en-US">
    <voice name="pt-BR-AntonioNeural">
        <prosody rate="0%" pitch="0%">Testing!</prosody>
</voice>
</speak>

and also:

The string is missing the terminator: '.
    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : TerminatorExpectedAtEndOfString
{"code":1,"message":"Command failed: powershell -NoProfile -command \"Add-Type -AssemblyName System.speech; $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; $speak.SpeakSsml(' <speak xmlns=\"http://www.w3.org/2001/10/synthesis\" xmlns:mstts=\"http://www.w3.org/2001/mstts\" xmlns:emo=\"http://www.w3.org/2009/10/emotionml\" version=\"1.0\" xml:lang=\"en-US\">\n    <voice name=\"pt-BR-AntonioNeural\">\n        <prosody rate=\"0%\" pitch=\"0%\">Testing!!</prosody>\n</voice>\n</speak> ')\"\nThe string is missing the terminator: '.\r\n    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException\r\n  ..."}

Is it valid to have new lines in the xml in the powershell command?

Also, show us how you have configured the exec node in the non-working case.

Got it. My browser is ALWAYS open on anything I have Node-Red working with (displays & controls, etc), so "simply works" is good'nuf for me.

Hi Gunner,

Could you kindly share the download link for the brower's TTS support?

There was no "link" to provide... I use Chrome and it "just worked" with the ui_audio node.

Note: even though there is no actual "visible" widget in the dashboard for this node, the dashboard must be open for it to work.