node-red-contrib-tts-ultimate 3.0.6 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,7 +1,25 @@
1
- ![Sample Node](img/logo.png)
1
+ ![Sample Node](img/logo.png)
2
2
 
3
- [![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg?style=flat-square)](https://www.paypal.me/techtoday)
3
+ [![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg?style=flat-square)](https://www.paypal.me/techtoday)
4
4
 
5
+ <b>Version 3.1.1</b> June 2026<br/>
6
+
7
+ - NEW: voice option fields (ElevenLabs Stability/Similarity/Style/Speed and Google Rate/Pitch) are now sliders showing the current value live.<br/>
8
+ </p>
9
+
10
+ <p>
11
+ <b>Version 3.1.0</b> June 2026<br/>
12
+
13
+ - NEW: refresh icon next to the "Voice" field to reload the voices list on demand.<br/>
14
+ - NEW: ElevenLabs models are now read dynamically from the API (with a refresh icon), so new models appear automatically. Model-specific options (Style Exaggeration, Speaker boost) are enabled/disabled based on the capabilities reported by ElevenLabs.<br/>
15
+ - NEW: ElevenLabs "Speed" option (0.7 - 1.2, default 1.0) for the v2 engine.<br/>
16
+ </p>
17
+
18
+ <p>
19
+ <b>Version 3.0.7</b> March 2026<br/>
20
+
21
+ - CHORE: fixed some issues with voice.ai.<br/>
22
+ </p>
5
23
 
6
24
  <p>
7
25
  <b>Version 3.0.6</b> March 2026<br/>
@@ -35,10 +53,8 @@
35
53
  - NEW: Added option to avoid resuming music if it was playing before TTS messages.<br/>
36
54
  </p>
37
55
 
38
- -----------------------------------------------------------------------
39
-
40
-
41
-
56
+ ---
57
+
42
58
  <p>
43
59
  <b>Version 2.0.10</b> November 2024<br/>
44
60
  - ElevenLabs V2: now you can set the additional parameters for the voice.<br/>
@@ -322,19 +338,16 @@
322
338
 
323
339
  # CHANGELOG BELOW COMES FROM OLD SONOSPOLLYTTS NODE
324
340
 
325
- >
326
- > ***UPDATE PATH FROM SONOSPOLLYTTS TO TTS-ULTIMATE***
341
+ > **_UPDATE PATH FROM SONOSPOLLYTTS TO TTS-ULTIMATE_**
327
342
  >
328
343
  > Supergiovane takes care about your brain and your time.<br/>
329
344
  > Install TTS-Ultimate. Both SonosPollyTTS and TTS-Ultimate can cohexist.<br/>
330
345
  > Then just delete your old SonosPollyTTS nodes and replace it with TTS-Ultimate nodes.<br/>
331
346
  > The cache will remain the same. Your own audio files and hailing files won't be touched. You'll find it again in TTS-Ultimate<br/>
332
- >
333
347
 
334
348
  <br/>
335
349
  <br/>
336
350
 
337
-
338
351
  <p>
339
352
  <b>Version 2.0.5</b> December 2020<br/>
340
353
  - FIX: if you pass a numeric value as payload, the node thows an error in the status and the payload is not handled.</br>
package/README.md CHANGED
@@ -1,12 +1,11 @@
1
-
2
- ![Sample Node](img/logo.png)
1
+ ![Sample Node](img/logo.png)
3
2
 
4
3
  [![NPM version][npm-version-image]][npm-url]
5
4
  [![NPM downloads per month][npm-downloads-month-image]][npm-url]
6
5
  [![NPM downloads total][npm-downloads-total-image]][npm-url]
7
6
  [![MIT License][license-image]][license-url]
8
7
  [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
9
- [![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg?style=flat-square)](https://www.paypal.me/techtoday)
8
+ [![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg?style=flat-square)](https://www.paypal.me/techtoday)
10
9
  [![Facebook][facebook-image]][facebook-url]
11
10
 
12
11
  <img src='https://raw.githubusercontent.com/Supergiovane/node-red-contrib-tts-ultimate/master/img/main.png' width="80%">
@@ -16,58 +15,214 @@
16
15
  > Adjust the nodes according to your setup
17
16
 
18
17
  ```js
19
- [{"id":"569773ae.930abc","type":"inject","z":"344c547c.b230c4","name":"","topic":"","payload":"true","payloadType":"bool","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":230,"y":300,"wires":[["e066ce90.46f758"]]},{"id":"e066ce90.46f758","type":"function","z":"344c547c.b230c4","name":"Via function","func":"// The simplest way\nmsg.payload=\"Benvenuti,Wilkommen,Wellcome!\";\nreturn msg;\n","outputs":1,"noerr":0,"x":370,"y":300,"wires":[["3d9635bc.53c14a"]]},{"id":"c272b47c.41e238","type":"inject","z":"344c547c.b230c4","name":"","topic":"","payload":"true","payloadType":"bool","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":230,"y":340,"wires":[["2fcffdb7.1c76ea"]]},{"id":"2fcffdb7.1c76ea","type":"function","z":"344c547c.b230c4","name":"Set volume","func":"// Set the Volume\nmsg.volume=\"60\"; // If not set, will take the volume from setting page\nmsg.payload=\"Benvenuti,Wilkommen,Wellcome!\";\nreturn msg;\n\n","outputs":1,"noerr":0,"x":370,"y":340,"wires":[["3d9635bc.53c14a"]]},{"id":"2bd6fd7f.9b9ae2","type":"inject","z":"344c547c.b230c4","name":"","topic":"","payload":"true","payloadType":"bool","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":230,"y":380,"wires":[["aa3b6e42.669fc"]]},{"id":"aa3b6e42.669fc","type":"function","z":"344c547c.b230c4","name":"Array of messages","func":"// Create an array of messages\nvar aMessages=[];\n// Add random messages\naMessages.push({volume:\"50\",payload:\"Benvenuti.\"});\n// Wheater in Italy\naMessages.push({volume:\"40\",payload:\"http://media.ilmeteo.it/audio/2020-12-23.mp3\"});\n// Add random messages\naMessages.push({volume:\"30\",payload:\"Cambia la tua voce nei settaggi.\"});\nreturn [aMessages];\n","outputs":1,"noerr":0,"x":390,"y":380,"wires":[["3d9635bc.53c14a"]]},{"id":"3e0d9b5c.fe01b4","type":"inject","z":"344c547c.b230c4","name":"Hello World","topic":"","payload":"Ciao Mondo! Come stai?","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":250,"y":260,"wires":[["3d9635bc.53c14a"]]},{"id":"42e6fab4.e8d154","type":"comment","z":"344c547c.b230c4","name":"Play text on Sonos. Single player or Group of players","info":"","x":360,"y":220,"wires":[]},{"id":"3d9635bc.53c14a","type":"ttsultimate","z":"344c547c.b230c4","name":"","voice":"Brian","ssml":false,"sonosipaddress":"192.168.1.109","sonosvolume":"30","sonoshailing":"Hailing_Hailing.mp3","config":"557d8082.eb5a8","property":"payload","propertyType":{},"rules":[],"x":610,"y":260,"wires":[[]]},{"id":"557d8082.eb5a8","type":"ttsultimate-config","z":"","name":"googletranslate","noderedipaddress":"192.168.1.219","noderedport":"1980","purgediratrestart":"leave","ttsservice":"googletranslate"}]
18
+ [
19
+ {
20
+ id: "569773ae.930abc",
21
+ type: "inject",
22
+ z: "344c547c.b230c4",
23
+ name: "",
24
+ topic: "",
25
+ payload: "true",
26
+ payloadType: "bool",
27
+ repeat: "",
28
+ crontab: "",
29
+ once: false,
30
+ onceDelay: 0.1,
31
+ x: 230,
32
+ y: 300,
33
+ wires: [["e066ce90.46f758"]],
34
+ },
35
+ {
36
+ id: "e066ce90.46f758",
37
+ type: "function",
38
+ z: "344c547c.b230c4",
39
+ name: "Via function",
40
+ func: '// The simplest way\nmsg.payload="Benvenuti,Wilkommen,Wellcome!";\nreturn msg;\n',
41
+ outputs: 1,
42
+ noerr: 0,
43
+ x: 370,
44
+ y: 300,
45
+ wires: [["3d9635bc.53c14a"]],
46
+ },
47
+ {
48
+ id: "c272b47c.41e238",
49
+ type: "inject",
50
+ z: "344c547c.b230c4",
51
+ name: "",
52
+ topic: "",
53
+ payload: "true",
54
+ payloadType: "bool",
55
+ repeat: "",
56
+ crontab: "",
57
+ once: false,
58
+ onceDelay: 0.1,
59
+ x: 230,
60
+ y: 340,
61
+ wires: [["2fcffdb7.1c76ea"]],
62
+ },
63
+ {
64
+ id: "2fcffdb7.1c76ea",
65
+ type: "function",
66
+ z: "344c547c.b230c4",
67
+ name: "Set volume",
68
+ func: '// Set the Volume\nmsg.volume="60"; // If not set, will take the volume from setting page\nmsg.payload="Benvenuti,Wilkommen,Wellcome!";\nreturn msg;\n\n',
69
+ outputs: 1,
70
+ noerr: 0,
71
+ x: 370,
72
+ y: 340,
73
+ wires: [["3d9635bc.53c14a"]],
74
+ },
75
+ {
76
+ id: "2bd6fd7f.9b9ae2",
77
+ type: "inject",
78
+ z: "344c547c.b230c4",
79
+ name: "",
80
+ topic: "",
81
+ payload: "true",
82
+ payloadType: "bool",
83
+ repeat: "",
84
+ crontab: "",
85
+ once: false,
86
+ onceDelay: 0.1,
87
+ x: 230,
88
+ y: 380,
89
+ wires: [["aa3b6e42.669fc"]],
90
+ },
91
+ {
92
+ id: "aa3b6e42.669fc",
93
+ type: "function",
94
+ z: "344c547c.b230c4",
95
+ name: "Array of messages",
96
+ func: '// Create an array of messages\nvar aMessages=[];\n// Add random messages\naMessages.push({volume:"50",payload:"Benvenuti."});\n// Wheater in Italy\naMessages.push({volume:"40",payload:"http://media.ilmeteo.it/audio/2020-12-23.mp3"});\n// Add random messages\naMessages.push({volume:"30",payload:"Cambia la tua voce nei settaggi."});\nreturn [aMessages];\n',
97
+ outputs: 1,
98
+ noerr: 0,
99
+ x: 390,
100
+ y: 380,
101
+ wires: [["3d9635bc.53c14a"]],
102
+ },
103
+ {
104
+ id: "3e0d9b5c.fe01b4",
105
+ type: "inject",
106
+ z: "344c547c.b230c4",
107
+ name: "Hello World",
108
+ topic: "",
109
+ payload: "Ciao Mondo! Come stai?",
110
+ payloadType: "str",
111
+ repeat: "",
112
+ crontab: "",
113
+ once: false,
114
+ onceDelay: 0.1,
115
+ x: 250,
116
+ y: 260,
117
+ wires: [["3d9635bc.53c14a"]],
118
+ },
119
+ {
120
+ id: "42e6fab4.e8d154",
121
+ type: "comment",
122
+ z: "344c547c.b230c4",
123
+ name: "Play text on Sonos. Single player or Group of players",
124
+ info: "",
125
+ x: 360,
126
+ y: 220,
127
+ wires: [],
128
+ },
129
+ {
130
+ id: "3d9635bc.53c14a",
131
+ type: "ttsultimate",
132
+ z: "344c547c.b230c4",
133
+ name: "",
134
+ voice: "Brian",
135
+ ssml: false,
136
+ sonosipaddress: "192.168.1.109",
137
+ sonosvolume: "30",
138
+ sonoshailing: "Hailing_Hailing.mp3",
139
+ config: "557d8082.eb5a8",
140
+ property: "payload",
141
+ propertyType: {},
142
+ rules: [],
143
+ x: 610,
144
+ y: 260,
145
+ wires: [[]],
146
+ },
147
+ {
148
+ id: "557d8082.eb5a8",
149
+ type: "ttsultimate-config",
150
+ z: "",
151
+ name: "googletranslate",
152
+ noderedipaddress: "192.168.1.219",
153
+ noderedport: "1980",
154
+ purgediratrestart: "leave",
155
+ ttsservice: "googletranslate",
156
+ },
157
+ ];
20
158
  ```
21
- </details>
22
159
 
160
+ </details>
23
161
 
24
162
  ## DESCRIPTION
163
+
25
164
  This node transforms a text into a speech audio that you can hear natively via <b>SONOS</b> speakers, but you can also simply create an audio file, without using SONOS at all.<br/>
26
165
  You can also generate an audio file for bluetooth speakers, web pages, etc.<br/>
27
166
  You can also use it with **your own audio file** as well and it can be used **totally offline** even without the use of TTS, without internet connection.<br/>
28
- The node can also create a ***TTS file (without the use of any Sonos device)***, to be read by third parties nodes.<br/>
29
- This is a major ***upgrade from the previously popular node SonosPollyTTS*** (SonosPollyTTS is not developed anymore).<br/>
167
+ The node can also create a **_TTS file (without the use of any Sonos device)_**, to be read by third parties nodes.<br/>
168
+ This is a major **_upgrade from the previously popular node SonosPollyTTS_** (SonosPollyTTS is not developed anymore).<br/>
30
169
 
170
+ ## SUPPORTED TTS ENGINES
171
+
172
+ <p>
173
+ <a href="https://www.npmjs.com/package/google-translate-tts" title="Google Translate TTS (free)">
174
+ <img src="https://cdn.simpleicons.org/googletranslate/4285F4" height="126" alt="Google Translate logo"/>
175
+ </a>&nbsp;&nbsp;
176
+ <a href="https://cloud.google.com/text-to-speech" title="Google Cloud Text-to-Speech">
177
+ <img src="https://cdn.simpleicons.org/googlecloud/4285F4" height="126" alt="Google Cloud logo"/>
178
+ </a>&nbsp;&nbsp;
179
+ <a href="https://elevenlabs.io" title="ElevenLabs">
180
+ <img src="https://cdn.simpleicons.org/elevenlabs/000000" height="126" alt="ElevenLabs logo"/>
181
+ </a>&nbsp;&nbsp;
182
+ <a href="https://voice.ai/docs/api-reference/text-to-speech/generate-speech" title="Voice.ai">
183
+ <img src="https://voice.ai/favicon.ico" height="126" alt="Voice.ai logo"/>
184
+ </a>
185
+ </p>
31
186
 
32
187
  [![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg?style=flat-square)](https://www.paypal.me/techtoday)
33
188
 
34
189
  ## CHANGELOG
35
- * See <a href="https://github.com/Supergiovane/node-red-contrib-tts-ultimate/blob/master/CHANGELOG.md">here the changelog</a>
190
+
191
+ - See <a href="https://github.com/Supergiovane/node-red-contrib-tts-ultimate/blob/master/CHANGELOG.md">here the changelog</a>
36
192
 
37
193
  ## FEATURES
38
- * **Native Sonos support**: hear the TTS audio directly via Sonos. You can also group speakers, set an hailing sound, choose the volume of each speaker etc.
39
- * **Output audio file**: the node can just create the TTS file to be used by other nodes. In this case, you doesn't need to use Sonos as player.
40
- * **Gooogle Translate Voices, Google TTS Voices and Elevenlabs.io voices** are all supported, with all avaiables languages and genders.
41
- * **Automatic grouping** is supported. You can group all players you want to play your announcements.
42
- * **Automatic discovery** of your players.
43
- * **Automatic resume of music** queue (including radio stations, but here, some users reports problem resuming ***radio stations*** and, because of lack of Sonos API documentation, the issue cannot currently be fixed), at exact track, at exact time. **Be aware that this could not work with all music queues**.
44
- * **TTS caching**. Elevenlabs and Google paid service, charges you if you use they tts service for a high rate of text to speech requests. TTS-Ultimate caches the TTS files. It downloads the TTS audio from Amazon or Google only once. The second time, the node will read it from the cache. The caches is resilient, that means it survives reboots and updates.
45
- * **Can work offline**. You can use your own audio files (with OwnFile node) to make the node works offline.
46
- * **UPLOAD your own audio files**. You can also upload your own audio files with OwnFile node.
47
194
 
195
+ - **Native Sonos support**: hear the TTS audio directly via Sonos. You can also group speakers, set an hailing sound, choose the volume of each speaker etc.
196
+ - **Output audio file**: the node can just create the TTS file to be used by other nodes. In this case, you doesn't need to use Sonos as player.
197
+ - **Google Translate Voices, Google TTS Voices, ElevenLabs voices and Voice.ai voices** are supported.
198
+ - **Automatic grouping** is supported. You can group all players you want to play your announcements.
199
+ - **Automatic discovery** of your players.
200
+ - **Automatic resume of music** queue (including radio stations, but here, some users reports problem resuming **_radio stations_** and, because of lack of Sonos API documentation, the issue cannot currently be fixed), at exact track, at exact time. **Be aware that this could not work with all music queues**.
201
+ - **TTS caching**. ElevenLabs and Google paid service charge you for a high rate of text-to-speech requests. TTS-Ultimate caches the TTS files: it downloads each generated audio only once, and then reads it from cache. The cache is resilient (survives reboots and updates).
202
+ - **Can work offline**. You can use your own audio files (with OwnFile node) to make the node works offline.
203
+ - **UPLOAD your own audio files**. You can also upload your own audio files with OwnFile node.
48
204
 
49
- ## BREAKING CHANGE ! BREAKING CHANGE ! BREAKING CHANGE ! BREAKING CHANGE !
205
+ ## BREAKING CHANGE !
50
206
 
51
207
  <p>
52
208
  <b>Version 3.0.0</b> April 2025<br/>
53
209
  - BREAKING CHANGE: Amazon Polly and Microsoft Azure TTS have been removed due to lack of time to update the old and complex API's. Anyone can add these again by forking the project and do a PR. Thank you!. If you still need those TTS, please stay or revert to 2.0.10.<br/>
54
210
  </p>
55
211
 
56
- -----------------------------------------------------------------------
212
+ ---
57
213
 
58
214
  <br/>
59
215
 
60
- >
61
- > ***NOTE IF YOU CANNOT UPLOAD YOUR OWN FILES***
216
+ > **_NOTE IF YOU CANNOT UPLOAD YOUR OWN FILES_**
62
217
  >
63
218
  > If you're running node-red as "plugin" for homeassistant, redmatic etc...<br/>
64
219
  > You may expect not to be able to upload your own files. Please check that the user running node-red, has premission **to write to the filesystem**.<br/>
65
- >
66
220
 
67
221
  <br/><br/>
68
222
 
69
223
  # TTS Service node
70
- Here you can set all parameters you need. All nodes will refer to this config node, so you need to set it only once.<br/>
224
+
225
+ Here you can set all parameters you need. All nodes will refer to this config node, so you need to set it only once.<br/>
71
226
  IF YOU RUN NODE-RED BEHIND DOCKER OR SOMETHING ELSE, BE AWARE: <br/>
72
227
  PORT USED BY THE NODE ARE 1980 (DEFAULT) AND 1400 (FOR SONOS DISCOVER). <br/>
73
228
  PLEASE ALLOW MDNS AND UDP AS WELL
@@ -78,34 +233,33 @@ For Google TTS Engine, you can choose pitch and speed rate of the voice.
78
233
  <br/>
79
234
  <br/>
80
235
 
81
- * **TTS Service using Google (without credentials)**<br/>
82
- This is the simplest way. Just select the voice and you're done. You don't need any credential and you don't even need to be registered to any google service. The voice list is more limited than other services, but it works without hassles.
83
- Note: long texts are automatically split into 200-character chunks (Google Translate TTS limit) and merged into a single audio output.
84
- Manual verify: `npm run verify:googletranslate-split -- --voice it-IT --text "..." --out ./out.mp3`
85
-
236
+ - **TTS Service using Google (without credentials)**<br/>
237
+ This is the simplest way. Just select the voice and you're done. You don't need any credential and you don't even need to be registered to any google service. The voice list is more limited than other services, but it works without hassles.
238
+ Note: long texts are automatically split into 200-character chunks (Google Translate TTS limit) and merged into a single audio output.
239
+ Manual verify: `npm run verify:googletranslate-split -- --voice it-IT --text "..." --out ./out.mp3`
240
+
86
241
  <br/>
87
242
 
88
- * **TTS Service using Google TTS**<br/>
89
- For Google TTS Engine, you can choose pitch and speed rate of the voice.<br/>
90
- **Google credentials file path**<br/>
91
- Here you must select your credential file, previously downloaded from Google, [with these steps](https://www.npmjs.com/package/@google-cloud/text-to-speech):
92
- > [Select or create a Cloud Platform project](https://console.cloud.google.com/project)<br/>
93
- > [Enable billing for your project](https://support.google.com/cloud/answer/6293499#enable-billing)<br/>
94
- > [Enable the Google Cloud Text-to-Speech API](https://console.cloud.google.com/flows/enableapi?apiid=texttospeech.googleapis.com)<br/>
95
-
243
+ - **TTS Service using Google TTS**<br/>
244
+ For Google TTS Engine, you can choose pitch and speed rate of the voice.<br/>
245
+ **Google credentials file path**<br/>
246
+ Here you must select your credential file, previously downloaded from Google, [with these steps](https://www.npmjs.com/package/@google-cloud/text-to-speech):
247
+ > [Select or create a Cloud Platform project](https://console.cloud.google.com/project)<br/>
248
+ > [Enable billing for your project](https://support.google.com/cloud/answer/6293499#enable-billing)<br/>
249
+ > [Enable the Google Cloud Text-to-Speech API](https://console.cloud.google.com/flows/enableapi?apiid=texttospeech.googleapis.com)<br/>
96
250
 
97
251
  <br/>
98
252
 
99
- * **TTS Service using ElevenLabs**<br/>
100
- Please use the V2 engine, as the V1 is deprecated and will not longer be supported. The V2 has multilingual voices and is more powerful.
101
- You have two choiches: To register to eventlabs, or not to register. If you don't register to elevenlabs.io, you will either have access on a limited amount of voices, or no access at all.
102
- After registration at elevenlabs.io, you can add any language to your personal list. The personal list will be then show in the node voice's list.<br/>
103
- <br/>
253
+ - **TTS Service using ElevenLabs**<br/>
254
+ Please use the V2 engine, as the V1 is deprecated and will not longer be supported. The V2 has multilingual voices and is more powerful.
255
+ You have two choiches: To register to eventlabs, or not to register. If you don't register to elevenlabs.io, you will either have access on a limited amount of voices, or no access at all.
256
+ After registration at elevenlabs.io, you can add any language to your personal list. The personal list will be then show in the node voice's list.<br/>
257
+ <br/>
104
258
 
105
- * **TTS Service using Voice.ai**<br/>
106
- Add your Voice.ai API key in the config node, deploy and restart Node-RED. The node will load your available voices and show them in the Voice dropdown.
107
- Note: SSML is not supported by this engine.
108
- <br/>
259
+ - **TTS Service using Voice.ai**<br/>
260
+ Add your Voice.ai API key in the config node, deploy and restart Node-RED. The node will load your available voices and show them in the Voice dropdown.
261
+ Note: SSML is not supported by this engine.
262
+ <br/>
109
263
 
110
264
  **Node-Red IP**<br/>
111
265
  set IP of your node-red machine. Write **AUTODISCOVER** to allow the node to auto discover your IP.
@@ -117,26 +271,26 @@ Note: if you use multiple `ttsultimate-config` nodes, each one now keeps its own
117
271
 
118
272
  **TTS Cache**
119
273
  <br/>
120
- ***Purge and delete the TTS cache folder at deploy or restart***<br/>
274
+ **_Purge and delete the TTS cache folder at deploy or restart_**<br/>
121
275
  On each deploy or node-red restart, delete all tts files in the cache. This is useful not to run out of disk space, in case you've a lot of TTS speech files.
122
276
  <br/>
123
- ***Leave the TTS cache folder untouched*** (suggested only if you have enough disk space)<br/>
277
+ **_Leave the TTS cache folder untouched_** (suggested only if you have enough disk space)<br/>
124
278
  Don't delete the files cached. Useful if you wish to keep the tts files, even in case of internet outages, node-red restart or reboots.
125
- <br/>
279
+ <br/>
126
280
 
127
281
  **Cache root folder**
128
282
  <br/>
129
283
  Set your preferred output folder for the files downloaded by the TTS Engine.<br/>
130
284
  This is useful if you wish to save the TTS cached files in a folder accessible, for example, by a third party web servers to serve an AirPlay2 speaker.<br/>
131
285
  Leave this field blank for the default.<br/>
132
- <br/>
286
+ <br/>
133
287
  <br/>
134
288
 
135
289
  # TTS-ULTIMATE NODE
136
290
 
137
291
  ## INPUT MESSAGES TO THE NODE <br/>
138
292
 
139
- *Examples*
293
+ _Examples_
140
294
 
141
295
  ```js
142
296
  // Play a message
@@ -154,14 +308,15 @@ return msg;
154
308
  ```js
155
309
  // Play a message with custom voice ID
156
310
  msg.payload = "Hello, the current temperature is 12°";
157
- msg.voiceId = 2
311
+ msg.voiceId = 2;
158
312
  return msg;
159
313
  ```
160
314
 
161
315
  ```js
162
316
  // Play smoke detection
163
317
  msg.sonoshailing = "SmokeAlert";
164
- msg.payload = "Warning, smoke detected. Fire extinguishers are in the kitchen, hall and garage.";
318
+ msg.payload =
319
+ "Warning, smoke detected. Fire extinguishers are in the kitchen, hall and garage.";
165
320
  return msg;
166
321
  ```
167
322
 
@@ -187,17 +342,16 @@ return msg;
187
342
 
188
343
  ## CHANGE CONFIGURATION VIA MSG PROPERTY
189
344
 
190
- You can change the configuration of tts-ultimate, *via msg.setConfig* property.<br/>
191
- The property is a JSON object.
345
+ You can change the configuration of tts-ultimate, _via msg.setConfig_ property.<br/>
346
+ The property is a JSON object.
192
347
 
193
348
  <img src='https://github.com/Supergiovane/node-red-contrib-tts-ultimate/raw/master/img/setConfig.png' width="80%">
194
349
 
195
-
196
350
  ```js
197
351
  // Set main player IP
198
352
  // The setting is retained until the node receives another msg.setConfig or until node-red is restarted.
199
- var config= {
200
- setMainPlayerIP:"192.168.1.109"
353
+ var config = {
354
+ setMainPlayerIP: "192.168.1.109",
201
355
  };
202
356
  msg.setConfig = config;
203
357
  return msg;
@@ -207,13 +361,13 @@ return msg;
207
361
  // Set player IP and additional players with their optional adapted volume, relative to the main sonos player volume.
208
362
  // You can specify the aditional player's volume adaptation
209
363
  // The setting is retained until the node receives another msg.setConfig or until node-red is restarted.
210
- var config= {
211
- setMainPlayerIP:"192.168.1.109",
212
- setPlayerGroupArray:[
213
- "192.168.1.110", // This additional player will use the same volume as the main sonos player.
214
- "192.168.1.111#-10", // This additional player will use the main sonos player's volume, minus 10.
215
- "192.168.1.112#20" // This additional player will use the main sonos player's volume, plus 20.
216
- ]
364
+ var config = {
365
+ setMainPlayerIP: "192.168.1.109",
366
+ setPlayerGroupArray: [
367
+ "192.168.1.110", // This additional player will use the same volume as the main sonos player.
368
+ "192.168.1.111#-10", // This additional player will use the main sonos player's volume, minus 10.
369
+ "192.168.1.112#20", // This additional player will use the main sonos player's volume, plus 20.
370
+ ],
217
371
  };
218
372
  msg.setConfig = config;
219
373
  return msg;
@@ -222,9 +376,9 @@ return msg;
222
376
  ```js
223
377
  // If you have only one additional player, without setting their adjusted volume.
224
378
  // The setting is retained until the node receives another msg.setConfig or until node-red is restarted.
225
- var config= {
226
- setMainPlayerIP:"192.168.1.109",
227
- setPlayerGroupArray:["192.168.1.110"]
379
+ var config = {
380
+ setMainPlayerIP: "192.168.1.109",
381
+ setPlayerGroupArray: ["192.168.1.110"],
228
382
  };
229
383
  msg.setConfig = config;
230
384
  return msg;
@@ -237,8 +391,112 @@ return msg;
237
391
  > Adjust the nodes according to your setup
238
392
 
239
393
  ```js
240
- [{"id":"4b4514d.047366c","type":"ttsultimate","z":"235d8e3d.a7583a","name":"","voice":"de-DE","ssml":false,"sonosipaddress":"192.168.1.109","sonosvolume":"5","sonoshailing":"0","config":"feee307e.54bca","property":"payload","propertyType":{},"rules":[],"x":430,"y":360,"wires":[["2b2d7556.251d0a"],["2978fe86.e680aa"]]},{"id":"2b2d7556.251d0a","type":"debug","z":"235d8e3d.a7583a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":610,"y":340,"wires":[]},{"id":"2978fe86.e680aa","type":"debug","z":"235d8e3d.a7583a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":610,"y":380,"wires":[]},{"id":"9d9e06be.09718","type":"function","z":"235d8e3d.a7583a","name":"Change Config","func":"// Set the main player IP and each IP belonging to the player's group\nvar config= {\n setMainPlayerIP:\"192.168.1.109\",\n setPlayerGroupArray:[\n \"192.168.1.110\",\n \"192.168.1.111\",\n \"192.168.1.112\"\n ]\n};\nmsg.setConfig = config;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":260,"y":360,"wires":[["4b4514d.047366c"]]},{"id":"c3da8b3a.e8f2c8","type":"inject","z":"235d8e3d.a7583a","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"Hello","payloadType":"str","x":110,"y":360,"wires":[["9d9e06be.09718"]]},{"id":"c55b7140.4a7cc8","type":"comment","z":"235d8e3d.a7583a","name":"Change the player and/or group of players via msg property.","info":"","x":270,"y":300,"wires":[]},{"id":"feee307e.54bca","type":"ttsultimate-config","name":"Config","noderedipaddress":"192.168.1.161","noderedport":"1980","purgediratrestart":"leave","ttsservice":"googletranslate"}]
394
+ [
395
+ {
396
+ id: "4b4514d.047366c",
397
+ type: "ttsultimate",
398
+ z: "235d8e3d.a7583a",
399
+ name: "",
400
+ voice: "de-DE",
401
+ ssml: false,
402
+ sonosipaddress: "192.168.1.109",
403
+ sonosvolume: "5",
404
+ sonoshailing: "0",
405
+ config: "feee307e.54bca",
406
+ property: "payload",
407
+ propertyType: {},
408
+ rules: [],
409
+ x: 430,
410
+ y: 360,
411
+ wires: [["2b2d7556.251d0a"], ["2978fe86.e680aa"]],
412
+ },
413
+ {
414
+ id: "2b2d7556.251d0a",
415
+ type: "debug",
416
+ z: "235d8e3d.a7583a",
417
+ name: "",
418
+ active: true,
419
+ tosidebar: true,
420
+ console: false,
421
+ tostatus: false,
422
+ complete: "true",
423
+ targetType: "full",
424
+ statusVal: "",
425
+ statusType: "auto",
426
+ x: 610,
427
+ y: 340,
428
+ wires: [],
429
+ },
430
+ {
431
+ id: "2978fe86.e680aa",
432
+ type: "debug",
433
+ z: "235d8e3d.a7583a",
434
+ name: "",
435
+ active: true,
436
+ tosidebar: true,
437
+ console: false,
438
+ tostatus: false,
439
+ complete: "true",
440
+ targetType: "full",
441
+ statusVal: "",
442
+ statusType: "auto",
443
+ x: 610,
444
+ y: 380,
445
+ wires: [],
446
+ },
447
+ {
448
+ id: "9d9e06be.09718",
449
+ type: "function",
450
+ z: "235d8e3d.a7583a",
451
+ name: "Change Config",
452
+ func: '// Set the main player IP and each IP belonging to the player\'s group\nvar config= {\n setMainPlayerIP:"192.168.1.109",\n setPlayerGroupArray:[\n "192.168.1.110",\n "192.168.1.111",\n "192.168.1.112"\n ]\n};\nmsg.setConfig = config;\nreturn msg;',
453
+ outputs: 1,
454
+ noerr: 0,
455
+ initialize: "",
456
+ finalize: "",
457
+ x: 260,
458
+ y: 360,
459
+ wires: [["4b4514d.047366c"]],
460
+ },
461
+ {
462
+ id: "c3da8b3a.e8f2c8",
463
+ type: "inject",
464
+ z: "235d8e3d.a7583a",
465
+ name: "",
466
+ props: [{ p: "payload" }],
467
+ repeat: "",
468
+ crontab: "",
469
+ once: false,
470
+ onceDelay: 0.1,
471
+ topic: "",
472
+ payload: "Hello",
473
+ payloadType: "str",
474
+ x: 110,
475
+ y: 360,
476
+ wires: [["9d9e06be.09718"]],
477
+ },
478
+ {
479
+ id: "c55b7140.4a7cc8",
480
+ type: "comment",
481
+ z: "235d8e3d.a7583a",
482
+ name: "Change the player and/or group of players via msg property.",
483
+ info: "",
484
+ x: 270,
485
+ y: 300,
486
+ wires: [],
487
+ },
488
+ {
489
+ id: "feee307e.54bca",
490
+ type: "ttsultimate-config",
491
+ name: "Config",
492
+ noderedipaddress: "192.168.1.161",
493
+ noderedport: "1980",
494
+ purgediratrestart: "leave",
495
+ ttsservice: "googletranslate",
496
+ },
497
+ ];
241
498
  ```
499
+
242
500
  </details>
243
501
 
244
502
  <br/>
@@ -255,8 +513,111 @@ return msg;
255
513
  > Adjust the nodes according to your setup
256
514
 
257
515
  ```js
258
- [{"id":"db0ea33.f1186e","type":"ttsultimate","z":"c6efd2b6.ab02e8","name":"","voice":"en-AU-Standard-A#en-AU#FEMALE","ssml":false,"sonosipaddress":"192.168.1.109","sonosvolume":"25","sonoshailing":"Hailing_Hailing.mp3","config":"4f941d61.f52c4c","propertyType":{},"rules":[],"x":670,"y":240,"wires":[[]]},{"id":"c7fb2970.271978","type":"ownfileultimate","z":"c6efd2b6.ab02e8","name":"","selectedFile":"OwnFile_Tur geoeffnet.mp3","x":490,"y":220,"wires":[["db0ea33.f1186e"]]},{"id":"fef80c5b.49f9e","type":"inject","z":"c6efd2b6.ab02e8","name":"","topic":"","payload":"true","payloadType":"bool","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":220,"wires":[["c7fb2970.271978"]]},{"id":"807f0f6c.6d59c","type":"comment","z":"c6efd2b6.ab02e8","name":"You can upload your own voice messages and use it with ttsultimate","info":"","x":310,"y":180,"wires":[]},{"id":"536e58b3.bb8468","type":"ownfileultimate","z":"c6efd2b6.ab02e8","name":"","selectedFile":"OwnFile_Tur geoeffnet.mp3","x":490,"y":260,"wires":[["db0ea33.f1186e"]]},{"id":"26c339f9.346fbe","type":"inject","z":"c6efd2b6.ab02e8","name":"","topic":"","payload":"true","payloadType":"bool","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":260,"wires":[["25016441.6447bc"]]},{"id":"25016441.6447bc","type":"function","z":"c6efd2b6.ab02e8","name":"Dynamically Select file","func":"// Override the selected file.\nmsg.selectedFile=\"Porta aperta\"\nreturn msg;","outputs":1,"noerr":0,"x":300,"y":260,"wires":[["536e58b3.bb8468"]]},{"id":"4f941d61.f52c4c","type":"ttsultimate-config","z":"","name":"GoogleTTS","noderedipaddress":"192.168.1.219","noderedport":"1980","purgediratrestart":"leave","ttsservice":"googletts"}]
516
+ [
517
+ {
518
+ id: "db0ea33.f1186e",
519
+ type: "ttsultimate",
520
+ z: "c6efd2b6.ab02e8",
521
+ name: "",
522
+ voice: "en-AU-Standard-A#en-AU#FEMALE",
523
+ ssml: false,
524
+ sonosipaddress: "192.168.1.109",
525
+ sonosvolume: "25",
526
+ sonoshailing: "Hailing_Hailing.mp3",
527
+ config: "4f941d61.f52c4c",
528
+ propertyType: {},
529
+ rules: [],
530
+ x: 670,
531
+ y: 240,
532
+ wires: [[]],
533
+ },
534
+ {
535
+ id: "c7fb2970.271978",
536
+ type: "ownfileultimate",
537
+ z: "c6efd2b6.ab02e8",
538
+ name: "",
539
+ selectedFile: "OwnFile_Tur geoeffnet.mp3",
540
+ x: 490,
541
+ y: 220,
542
+ wires: [["db0ea33.f1186e"]],
543
+ },
544
+ {
545
+ id: "fef80c5b.49f9e",
546
+ type: "inject",
547
+ z: "c6efd2b6.ab02e8",
548
+ name: "",
549
+ topic: "",
550
+ payload: "true",
551
+ payloadType: "bool",
552
+ repeat: "",
553
+ crontab: "",
554
+ once: false,
555
+ onceDelay: 0.1,
556
+ x: 130,
557
+ y: 220,
558
+ wires: [["c7fb2970.271978"]],
559
+ },
560
+ {
561
+ id: "807f0f6c.6d59c",
562
+ type: "comment",
563
+ z: "c6efd2b6.ab02e8",
564
+ name: "You can upload your own voice messages and use it with ttsultimate",
565
+ info: "",
566
+ x: 310,
567
+ y: 180,
568
+ wires: [],
569
+ },
570
+ {
571
+ id: "536e58b3.bb8468",
572
+ type: "ownfileultimate",
573
+ z: "c6efd2b6.ab02e8",
574
+ name: "",
575
+ selectedFile: "OwnFile_Tur geoeffnet.mp3",
576
+ x: 490,
577
+ y: 260,
578
+ wires: [["db0ea33.f1186e"]],
579
+ },
580
+ {
581
+ id: "26c339f9.346fbe",
582
+ type: "inject",
583
+ z: "c6efd2b6.ab02e8",
584
+ name: "",
585
+ topic: "",
586
+ payload: "true",
587
+ payloadType: "bool",
588
+ repeat: "",
589
+ crontab: "",
590
+ once: false,
591
+ onceDelay: 0.1,
592
+ x: 130,
593
+ y: 260,
594
+ wires: [["25016441.6447bc"]],
595
+ },
596
+ {
597
+ id: "25016441.6447bc",
598
+ type: "function",
599
+ z: "c6efd2b6.ab02e8",
600
+ name: "Dynamically Select file",
601
+ func: '// Override the selected file.\nmsg.selectedFile="Porta aperta"\nreturn msg;',
602
+ outputs: 1,
603
+ noerr: 0,
604
+ x: 300,
605
+ y: 260,
606
+ wires: [["536e58b3.bb8468"]],
607
+ },
608
+ {
609
+ id: "4f941d61.f52c4c",
610
+ type: "ttsultimate-config",
611
+ z: "",
612
+ name: "GoogleTTS",
613
+ noderedipaddress: "192.168.1.219",
614
+ noderedport: "1980",
615
+ purgediratrestart: "leave",
616
+ ttsservice: "googletts",
617
+ },
618
+ ];
259
619
  ```
620
+
260
621
  </details>
261
622
 
262
623
  This node allow you to upload your custom message and play it via ttsultimate without the need of an internet connection. You can use it, for example, with your alarm panel, to annuce a zone breach, a doorbell or so.
@@ -269,9 +630,9 @@ Select a file to be played. You can upload one or multiple files at the same tim
269
630
 
270
631
  **Priority**<br/>
271
632
  If set to <b>true</b>, the OwnFile message will cancel the current TTS queue, will stop the current phrase being spoken and the TTS-Ultimate node will play this priority message.<br/>
272
- Please refer to *msg.priority* msg input property of TTS-Ultimate for info on how this message will be handled<br/>
633
+ Please refer to _msg.priority_ msg input property of TTS-Ultimate for info on how this message will be handled<br/>
273
634
 
274
- ## INPUT MESSAGE
635
+ ## INPUT MESSAGE
275
636
 
276
637
  **msg.payload = true**<br/>
277
638
  Begin play of the message <br/>
@@ -281,11 +642,10 @@ Overrides the selected message and plays the filename you passed in. Please doub
281
642
 
282
643
  **msg.priority**<br/>
283
644
  If set to <b>true</b>, the OwnFile message will cancel the current TTS queue, will stop the current phrase being spoken and the TTS-Ultimate node will play this priority message.<br/>
284
- Please refer to *msg.priority* msg input property of TTS-Ultimate for info on how this message will be handled<br/>
285
-
645
+ Please refer to _msg.priority_ msg input property of TTS-Ultimate for info on how this message will be handled<br/>
286
646
 
287
647
  ![Logo](https://raw.githubusercontent.com/Supergiovane/node-red-contrib-tts-ultimate/master/img/madeinitaly.png)
288
-
648
+
289
649
  [license-image]: https://img.shields.io/badge/license-MIT-blue.svg
290
650
  [license-url]: https://github.com/Supergiovane/node-red-contrib-tts-ultimate/master/LICENSE
291
651
  [npm-url]: https://npmjs.org/package/node-red-contrib-tts-ultimate
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-tts-ultimate",
3
- "version": "3.0.6",
3
+ "version": "3.1.1",
4
4
  "description": "Transforms the text in speech and hear it using Sonos player or generate an audio file to be used with third parties nodes. Works with voices from Google (without credentials as well), Google TTS, ElevenLabs.io TTS, Voice.ai TTS or your own voice. You can also only create a TTS file to be read by third party nodes. Update of the popular SonosPollyTTS node.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -13,11 +13,8 @@
13
13
  },
14
14
  "keywords": [
15
15
  "node-red",
16
- "polly",
17
16
  "google",
18
17
  "voice",
19
- "amazon",
20
- "azure",
21
18
  "tts",
22
19
  "sonos",
23
20
  "IOT",
@@ -57,4 +54,4 @@
57
54
  "engines": {
58
55
  "node": ">=22.0.0"
59
56
  }
60
- }
57
+ }
@@ -505,6 +505,46 @@ module.exports = function (RED) {
505
505
 
506
506
  });
507
507
 
508
+ // 10/06/2026 Supergiovane, get the available ElevenLabs models with their capabilities.
509
+ // Used to dynamically populate the Model dropdown and enable/disable model-specific options.
510
+ RED.httpAdmin.get("/ttsgetmodels" + encodeURIComponent(node.id), RED.auth.needsPermission('TTSConfigNode.read'), function (req, res) {
511
+ const ttsservice = req.query.ttsservice || node.ttsservice;
512
+ if (!ttsservice || !ttsservice.includes("elevenlabs")) {
513
+ return res.json([]);
514
+ }
515
+ const apiKey = node.credentials.elevenlabsKey;
516
+ if (!apiKey || apiKey.trim() === "") {
517
+ return res.json([{ model_id: "", name: "ElevenLabs API key missing. Please configure, deploy and restart node-red.", error: true }]);
518
+ }
519
+ (async () => {
520
+ try {
521
+ const r = await fetch("https://api.elevenlabs.io/v1/models", {
522
+ method: "GET",
523
+ headers: { "xi-api-key": apiKey }
524
+ });
525
+ if (!r.ok) {
526
+ const body = await r.text().catch(() => "");
527
+ throw new Error(`HTTP ${r.status} ${r.statusText}${body ? " - " + body : ""}`);
528
+ }
529
+ const models = await r.json();
530
+ const list = (Array.isArray(models) ? models : [])
531
+ .filter(m => m && m.model_id && m.can_do_text_to_speech !== false)
532
+ .map(m => ({
533
+ model_id: m.model_id,
534
+ name: m.name || m.model_id,
535
+ can_use_style: m.can_use_style === true,
536
+ can_use_speaker_boost: m.can_use_speaker_boost === true,
537
+ maximum_text_length_per_request: m.maximum_text_length_per_request,
538
+ languages: Array.isArray(m.languages) ? m.languages.map(l => l.name || l.language_id).filter(Boolean) : []
539
+ }));
540
+ res.json(list);
541
+ } catch (error) {
542
+ RED.log.error('ttsultimate-config ' + node.id + ': Error getting ElevenLabs models: ' + error.message);
543
+ res.json([{ model_id: "", name: "Error getting ElevenLabs models: " + error.message + " Check credentials, deploy and restart node-red.", error: true }]);
544
+ }
545
+ })();
546
+ });
547
+
508
548
  // ########################################################
509
549
  //#endregion
510
550
 
@@ -1,4 +1,14 @@
1
1
  <script type="text/html" data-template-name="ttsultimate">
2
+ <style>
3
+ .ttsSliderValue {
4
+ display: inline-block;
5
+ min-width: 34px;
6
+ margin-left: 8px;
7
+ text-align: right;
8
+ font-weight: bold;
9
+ vertical-align: middle;
10
+ }
11
+ </style>
2
12
  <div class="form-row">
3
13
  <b>TTS Ultimate configuration</b>&nbsp&nbsp&nbsp&nbsp<span style="color:red"><i class="fa fa-question-circle"></i>&nbsp<a target="_blank" href="https://github.com/Supergiovane/node-red-contrib-tts-ultimate"><u>Help online</u></a></span>
4
14
  <br/>
@@ -17,48 +27,60 @@
17
27
  <div id="allGUI">
18
28
  <div class="form-row">
19
29
  <label for="node-input-voice"><i class="icon-tag"></i> Voice</label>
20
- <select id="node-input-voice">
30
+ <select id="node-input-voice" style="width:60%">
21
31
  <option value='Joanna'>Joanna (en-US)</option>
22
32
  </select>
33
+ <a id="refreshVoices" class="red-ui-button" title="Refresh voices list" style="margin-left:5px"><i class="fa fa-refresh"></i></a>
23
34
  </div>
24
35
 
25
36
  <div id = "divGoogleTTSAudioConfig">
26
37
  <div class="form-row">
27
38
  <label for="node-input-speakingrate"><i class="fa fa-volume-up"></i> Rate</label>
28
- <input type="text" id="node-input-speakingrate" style="width:60px"> (Between 0.25 and 4.0, default 1)
39
+ <input type="range" id="node-input-speakingrate" min="0.25" max="4.0" step="0.05" style="width:50%; vertical-align:middle;">
40
+ <span class="ttsSliderValue" id="val-speakingrate">1</span> <small>(0.25 - 4.0, default 1)</small>
29
41
  </div>
30
42
  <div class="form-row">
31
43
  <label for="node-input-speakingpitch"><i class="fa fa-volume-up"></i> Pitch</label>
32
- <input type="text" id="node-input-speakingpitch" style="width:60px"> (Between -20.0 and 20.0, default 0)
44
+ <input type="range" id="node-input-speakingpitch" min="-20" max="20" step="0.5" style="width:50%; vertical-align:middle;">
45
+ <span class="ttsSliderValue" id="val-speakingpitch">0</span> <small>(-20.0 - 20.0, default 0)</small>
33
46
  </div>
34
47
  </div>
35
48
  <div id="divElevenLabsOptions" hidden>
36
49
  <div class="form-row">
37
50
  <label for="node-input-elevenlabsStability"><i class="fa fa-volume-up"></i> Stability</label>
38
- <input type="text" id="node-input-elevenlabsStability" style="width:60px"> (default 0.5)
51
+ <input type="range" id="node-input-elevenlabsStability" min="0" max="1" step="0.05" style="width:50%; vertical-align:middle;">
52
+ <span class="ttsSliderValue" id="val-elevenlabsStability">0.5</span> <small>(default 0.5)</small>
39
53
  </div>
40
54
  <div class="form-row">
41
55
  <label for="node-input-elevenlabsSimilarity_boost"><i class="fa fa-volume-up"></i> Similarity boost</label>
42
- <input type="text" id="node-input-elevenlabsSimilarity_boost" style="width:60px"> (Default 0.5)
56
+ <input type="range" id="node-input-elevenlabsSimilarity_boost" min="0" max="1" step="0.05" style="width:50%; vertical-align:middle;">
57
+ <span class="ttsSliderValue" id="val-elevenlabsSimilarity_boost">0.5</span> <small>(default 0.5)</small>
43
58
  </div>
44
- <div class="form-row">
59
+ <div class="form-row" id="divElevenLabsStyle">
45
60
  <label for="node-input-elevenlabsStyle"><i class="fa fa-volume-up"></i> Style Exaggeration </label>
46
- <input type="text" id="node-input-elevenlabsStyle" style="width:60px"> (Default 0.0)
61
+ <input type="range" id="node-input-elevenlabsStyle" min="0" max="1" step="0.05" style="width:50%; vertical-align:middle;">
62
+ <span class="ttsSliderValue" id="val-elevenlabsStyle">0.0</span> <small>(default 0.0)</small>
47
63
  </div>
48
64
  <div class="form-row">
65
+ <label for="node-input-elevenlabsSpeed"><i class="fa fa-tachometer"></i> Speed</label>
66
+ <input type="range" id="node-input-elevenlabsSpeed" min="0.7" max="1.2" step="0.05" style="width:50%; vertical-align:middle;">
67
+ <span class="ttsSliderValue" id="val-elevenlabsSpeed">1.0</span> <small>(0.7 - 1.2, default 1.0)</small>
68
+ </div>
69
+ <div class="form-row" id="divElevenLabsSpeakerBoost">
49
70
  <label></label>
50
71
  <input type="checkbox" id="node-input-elevenlabsUse_speaker_boost" style="margin-left: 0px; vertical-align: top; width: auto !important;"> <label style="width:auto !important;"> Speaker boost</label>
51
72
  </div>
52
73
  <div class="form-row">
53
74
  <label for="node-input-elevenlabsModel"><i class="fa fa-cubes"></i> Model</label>
54
- <select id="node-input-elevenlabsModel" style="width:60%">
55
- <option value="">Automatic</option>
75
+ <select id="node-input-elevenlabsModel" style="width:55%">
76
+ <option value="">Automatic (recommended)</option>
56
77
  <option value="eleven_monolingual_v1">Eleven Monolingual v1</option>
57
78
  <option value="eleven_multilingual_v1">Eleven Multilingual v1</option>
58
79
  <option value="eleven_multilingual_v2">Eleven Multilingual v2</option>
59
80
  <option value="eleven_turbo_v2">Eleven Turbo v2</option>
60
81
  <option value="eleven_turbo_v2_5">Eleven Turbo v2.5</option>
61
82
  </select>
83
+ <a id="refreshElevenLabsModels" class="red-ui-button" title="Refresh models list" style="margin-left:5px"><i class="fa fa-refresh"></i></a>
62
84
  </div>
63
85
  <div class="form-row">
64
86
  <label for="node-input-elevenlabsOptimizeLatency"><i class="fa fa-tachometer"></i> Latency preset</label>
@@ -201,6 +223,7 @@
201
223
  elevenlabsStability: { value: "0.5", required: false },
202
224
  elevenlabsSimilarity_boost: { value: "0.5", required: false },
203
225
  elevenlabsStyle: { value: "0.0", required: false },
226
+ elevenlabsSpeed: { value: "1.0", required: false },
204
227
  elevenlabsUse_speaker_boost: { value: true, required: false },
205
228
  elevenlabsModel: { value: "", required: false },
206
229
  elevenlabsOptimizeLatency: { value: "", required: false },
@@ -230,6 +253,7 @@
230
253
  var node = this;
231
254
  var oNodeServer = RED.nodes.node($("#node-input-config").val()); // Store the config-node
232
255
  if (node.elevenlabsModel !== undefined) $("#node-input-elevenlabsModel").val(node.elevenlabsModel);
256
+ if (node.elevenlabsSpeed !== undefined) $("#node-input-elevenlabsSpeed").val(node.elevenlabsSpeed);
233
257
  if (node.elevenlabsOptimizeLatency !== undefined) $("#node-input-elevenlabsOptimizeLatency").val(node.elevenlabsOptimizeLatency);
234
258
  if (node.elevenlabsOutputFormat !== undefined) $("#node-input-elevenlabsOutputFormat").val(node.elevenlabsOutputFormat);
235
259
  if (node.elevenlabsSeed !== undefined) $("#node-input-elevenlabsSeed").val(node.elevenlabsSeed);
@@ -264,6 +288,7 @@
264
288
  try {
265
289
  oNodeServer = RED.nodes.node($(this).val());
266
290
  getVoices();
291
+ getElevenLabsModels();
267
292
  updateTtsOptionsVisibility();
268
293
  } catch (error) {
269
294
  }
@@ -363,8 +388,93 @@
363
388
  }
364
389
  updateTtsOptionsVisibility();
365
390
  getVoices();
391
+
392
+ // Refresh voices list on demand
393
+ $("#refreshVoices").click(function (e) {
394
+ e.preventDefault();
395
+ if (oNodeServer === null || oNodeServer === undefined || !oNodeServer.ttsservice) {
396
+ RED.notify("Please select a valid TTS service first.", { type: 'warning', timeout: 3000 });
397
+ return;
398
+ }
399
+ getVoices();
400
+ });
366
401
  // #####################################
367
402
 
403
+ // ElevenLabs: read models and their capabilities from the API, then propose them to the user.
404
+ // Options not supported by the selected model are automatically disabled.
405
+ // #####################################
406
+ function applyElevenLabsModelCapabilities() {
407
+ var $opt = $("#node-input-elevenlabsModel option:selected");
408
+ var modelVal = $("#node-input-elevenlabsModel").val();
409
+ // "Automatic" (empty) or models loaded without capability data => keep everything enabled.
410
+ var styleSupported = (modelVal === "" || modelVal === null) ? true : ($opt.data("style") !== false);
411
+ var boostSupported = (modelVal === "" || modelVal === null) ? true : ($opt.data("boost") !== false);
412
+ toggleElevenLabsRow("#divElevenLabsStyle", "#node-input-elevenlabsStyle", styleSupported);
413
+ toggleElevenLabsRow("#divElevenLabsSpeakerBoost", "#node-input-elevenlabsUse_speaker_boost", boostSupported);
414
+ }
415
+ function toggleElevenLabsRow(rowSelector, inputSelector, supported) {
416
+ if (supported) {
417
+ $(rowSelector).css("opacity", "1");
418
+ $(inputSelector).prop("disabled", false);
419
+ } else {
420
+ $(rowSelector).css("opacity", "0.4");
421
+ $(inputSelector).prop("disabled", true);
422
+ }
423
+ }
424
+
425
+ function getElevenLabsModels() {
426
+ if (oNodeServer === null || oNodeServer === undefined || !oNodeServer.ttsservice || oNodeServer.ttsservice.indexOf("elevenlabs") === -1) return;
427
+ $.getJSON("ttsgetmodels" + encodeURIComponent(oNodeServer.id) + "?ttsservice=" + oNodeServer.ttsservice, new Date().getTime(), (data) => {
428
+ if (!Array.isArray(data)) return;
429
+ // Backend signalled an error/missing key: keep the static fallback list and warn the user.
430
+ if (data.length === 1 && data[0].error) {
431
+ RED.notify(data[0].name, { type: 'warning', timeout: 6000 });
432
+ return;
433
+ }
434
+ if (data.length === 0) return;
435
+ var $sel = $("#node-input-elevenlabsModel");
436
+ $sel.find('option').remove().end();
437
+ $sel.append($("<option></option>").attr("value", "").text("Automatic (recommended)"));
438
+ data.forEach(m => {
439
+ var $o = $("<option></option>").attr("value", m.model_id).text(m.name);
440
+ $o.attr("data-style", m.can_use_style === true);
441
+ $o.attr("data-boost", m.can_use_speaker_boost === true);
442
+ if (m.languages && m.languages.length) $o.attr("title", "Languages: " + m.languages.join(", "));
443
+ $sel.append($o);
444
+ });
445
+ $sel.val(node.elevenlabsModel !== undefined ? node.elevenlabsModel : "");
446
+ if ($sel.val() === null) $sel.val(""); // Saved model no longer offered by the API
447
+ applyElevenLabsModelCapabilities();
448
+ });
449
+ }
450
+ getElevenLabsModels();
451
+
452
+ $("#node-input-elevenlabsModel").change(applyElevenLabsModelCapabilities);
453
+
454
+ // Refresh ElevenLabs models list on demand
455
+ $("#refreshElevenLabsModels").click(function (e) {
456
+ e.preventDefault();
457
+ if (oNodeServer === null || oNodeServer === undefined || !oNodeServer.ttsservice || oNodeServer.ttsservice.indexOf("elevenlabs") === -1) {
458
+ RED.notify("Please select an ElevenLabs TTS service first.", { type: 'warning', timeout: 3000 });
459
+ return;
460
+ }
461
+ getElevenLabsModels();
462
+ });
463
+ // #####################################
464
+
465
+ // Voice options sliders: show the current value live next to each slider.
466
+ // #####################################
467
+ function setupVoiceOptionSlider(id) {
468
+ var $input = $("#node-input-" + id);
469
+ var $out = $("#val-" + id);
470
+ if ($input.length === 0 || $out.length === 0) return;
471
+ var update = function () { $out.text($input.val()); };
472
+ $input.on("input change", update);
473
+ update();
474
+ }
475
+ ["speakingrate", "speakingpitch", "elevenlabsStability", "elevenlabsSimilarity_boost", "elevenlabsStyle", "elevenlabsSpeed"].forEach(setupVoiceOptionSlider);
476
+ // #####################################
477
+
368
478
  // Refresh the combo
369
479
  // #####################################
370
480
  node.refreshHailingList = () => {
@@ -638,7 +748,9 @@
638
748
  | Stability | Only avaiable with Elevenlabs. Please refer to Elevenlabs.io description. Values from 0 to 1 (for example 0.2, 0.7, etc.) |
639
749
  | Similarity | Only avaiable with Elevenlabs. Please refer to Elevenlabs.io description. Values from 0 to 1 (for example 0.2, 0.7, etc.) |
640
750
  | Style Exaggeration | Only avaiable with Elevenlabs. Please refer to Elevenlabs.io description. Values from 0 to 1 (for example 0.2, 0.7, etc.)|
751
+ | Speed | Only avaiable with Elevenlabs v2. Controls the speaking speed. Values between 0.7 (slower) and 1.2 (faster), default 1.0. |
641
752
  | Speaker boost | Only avaiable with Elevenlabs. Please refer to Elevenlabs.io description. Values from 0 to 1 (for example 0.2, 0.7, etc.) |
753
+ | Model | The model list is read directly from ElevenLabs (press the refresh icon to reload it), so new models appear automatically. Options not supported by the selected model (such as Style Exaggeration or Speaker boost) are automatically disabled, based on the model capabilities reported by ElevenLabs. Leave on "Automatic" to let the node pick a sensible default. |
642
754
 
643
755
  <br/>
644
756
 
@@ -746,6 +746,7 @@ module.exports = function (RED) {
746
746
  const stability = config.elevenlabsStability !== undefined && config.elevenlabsStability !== "" ? Number(config.elevenlabsStability) : undefined;
747
747
  const similarity = config.elevenlabsSimilarity_boost !== undefined && config.elevenlabsSimilarity_boost !== "" ? Number(config.elevenlabsSimilarity_boost) : undefined;
748
748
  const style = config.elevenlabsStyle !== undefined && config.elevenlabsStyle !== "" ? Number(config.elevenlabsStyle) : undefined;
749
+ const speed = config.elevenlabsSpeed !== undefined && config.elevenlabsSpeed !== "" ? Number(config.elevenlabsSpeed) : undefined;
749
750
  const resolvedModel = config.elevenlabsModel && config.elevenlabsModel !== "" ? config.elevenlabsModel : "eleven_multilingual_v2";
750
751
  const latencyPreset = config.elevenlabsOptimizeLatency && config.elevenlabsOptimizeLatency !== "" ? config.elevenlabsOptimizeLatency : undefined;
751
752
  const outputFormat = config.elevenlabsOutputFormat && config.elevenlabsOutputFormat !== "" ? config.elevenlabsOutputFormat : undefined;
@@ -761,6 +762,7 @@ module.exports = function (RED) {
761
762
  if (stability !== undefined && !Number.isNaN(stability)) params.voice_settings.stability = stability;
762
763
  if (similarity !== undefined && !Number.isNaN(similarity)) params.voice_settings.similarity_boost = similarity;
763
764
  if (style !== undefined && !Number.isNaN(style)) params.voice_settings.style = style;
765
+ if (speed !== undefined && !Number.isNaN(speed)) params.voice_settings.speed = speed;
764
766
  params.voice_settings.use_speaker_boost = useSpeakerBoost;
765
767
  if (Object.keys(params.voice_settings).length === 0) delete params.voice_settings;
766
768
  if (latencyPreset !== undefined) params.optimize_streaming_latency = latencyPreset;