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

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
@@ -3,6 +3,17 @@
3
3
  [![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg?style=flat-square)](https://www.paypal.me/techtoday)
4
4
 
5
5
 
6
+ <p>
7
+ <b>Version 3.0.6</b> March 2026<br/>
8
+ - Fix: Voice.ai voice list parsing (now shows all available voices).<br/>
9
+ - Change: removed legacy/removed engines (Polly/Azure) from the config UI and docs.<br/>
10
+ </p>
11
+
12
+ <p>
13
+ <b>Version 3.0.5</b> March 2026<br/>
14
+ - NEW: Voice.ai TTS engine.<br/>
15
+ </p>
16
+
6
17
  <p>
7
18
  <b>Version 3.0.4</b> January 2026<br/>
8
19
  - Fix: cache purge at restart/deploy is now isolated per `ttsultimate-config` node (no more deleting other config caches).<br/>
package/README.md CHANGED
@@ -73,22 +73,11 @@ PORT USED BY THE NODE ARE 1980 (DEFAULT) AND 1400 (FOR SONOS DISCOVER). <br/>
73
73
  PLEASE ALLOW MDNS AND UDP AS WELL
74
74
 
75
75
  **TTS Service**<br/>
76
- You can choose between Elevenlabs.io, Google (without credentials), Google TTS (require credentials and registration to google).<br/>
76
+ You can choose between Voice.ai, Elevenlabs.io, Google (without credentials), Google TTS (require credentials and registration to google).<br/>
77
77
  For Google TTS Engine, you can choose pitch and speed rate of the voice.
78
78
  <br/>
79
79
  <br/>
80
80
 
81
- * **TTS Service using Amazon AWS (Polly)**<br/>
82
-
83
- (REMOVED IN v3.0.0 AND NOT USED ANYMORE)
84
-
85
- !IF YOU NEED THIS SERVICE, INSTALL ANY VERSION < 3.0.0 (ANY 2.x.x IS FINE)!
86
- > ``` npm install node-red-contrib-tts-ultimate@2.0.10 ```
87
-
88
- [Navigate here go here to view the old version](https://www.npmjs.com/package/node-red-contrib-tts-ultimate/v/2.0.10)
89
-
90
- <br/>
91
-
92
81
  * **TTS Service using Google (without credentials)**<br/>
93
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.
94
83
  Note: long texts are automatically split into 200-character chunks (Google Translate TTS limit) and merged into a single audio output.
@@ -105,18 +94,6 @@ For Google TTS Engine, you can choose pitch and speed rate of the voice.
105
94
  > [Enable the Google Cloud Text-to-Speech API](https://console.cloud.google.com/flows/enableapi?apiid=texttospeech.googleapis.com)<br/>
106
95
 
107
96
 
108
- <br/>
109
-
110
- * **TTS Service using Microsot Azure TTS**
111
-
112
- (REMOVED IN v3.0.0 AND NOT USED ANYMORE)
113
-
114
- !IF YOU NEED THIS SERVICE, INSTALL ANY VERSION < 3.0.0 (ANY 2.x.x IS FINE)!
115
- > ``` npm install node-red-contrib-tts-ultimate@2.0.10 ```
116
-
117
- [Navigate here go here to view the old version](https://www.npmjs.com/package/node-red-contrib-tts-ultimate/v/2.0.10)
118
-
119
-
120
97
  <br/>
121
98
 
122
99
  * **TTS Service using ElevenLabs**<br/>
@@ -125,6 +102,11 @@ For Google TTS Engine, you can choose pitch and speed rate of the voice.
125
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/>
126
103
  <br/>
127
104
 
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/>
109
+
128
110
  **Node-Red IP**<br/>
129
111
  set IP of your node-red machine. Write **AUTODISCOVER** to allow the node to auto discover your IP.
130
112
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "node-red-contrib-tts-ultimate",
3
- "version": "3.0.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 Amazon, Google (without credentials as well), Microsoft TTS Azure, ElevenLabs.io 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.",
3
+ "version": "3.0.6",
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": {
7
7
  "test": "test",
@@ -25,7 +25,8 @@
25
25
  "ttsultimate",
26
26
  "sonospollytts",
27
27
  "neural",
28
- "elevenlabs"
28
+ "elevenlabs",
29
+ "voiceai"
29
30
  ],
30
31
  "node-red": {
31
32
  "nodes": {
@@ -1,9 +1,9 @@
1
1
  <script type="text/javascript">
2
2
 
3
- RED.nodes.registerType("ttsultimate-config", {
4
- category: 'config',
5
- defaults:
6
- {
3
+ RED.nodes.registerType("ttsultimate-config", {
4
+ category: 'config',
5
+ defaults:
6
+ {
7
7
  name: { value: "TTS Service" },
8
8
  noderedipaddress:
9
9
  {
@@ -21,18 +21,15 @@
21
21
  ttsservice: { value: "googletranslate", required: false },
22
22
  TTSRootFolderPath: { value: "", required: false }
23
23
 
24
- },
25
- credentials: {
26
- accessKey: { type: "text" },
27
- secretKey: { type: "password" },
28
- mssubscriptionKey: { type: "text" },
29
- mslocation: { type: "text" },
30
- elevenlabsKey: { type: "text" }
31
- },
32
- label: function () {
33
- return this.name || "";
34
- },
35
- oneditprepare: function () {
24
+ },
25
+ credentials: {
26
+ elevenlabsKey: { type: "text" },
27
+ voiceaiKey: { type: "text" }
28
+ },
29
+ label: function () {
30
+ return this.name || "";
31
+ },
32
+ oneditprepare: function () {
36
33
  var node = this;
37
34
 
38
35
  // 21/03/2020 Check if the node is the absolute first in the flow. In this case, it has no http server instatiaced
@@ -60,37 +57,29 @@
60
57
  $("#node-config-input-purgediratrestart").val("leave");
61
58
  }
62
59
 
63
- // 22/12/2020 Hide parts of the ui
64
- // ##########################################################
65
- $("#node-config-input-ttsservice").change(function (e) {
66
- if ($("#node-config-input-ttsservice").val() === "polly") {
67
- $("#GoogleForm").hide();
68
- $("#microsoftAzureForm").hide();
69
- $("#elevenlabsForm").hide();
70
- $("#pollyForm").show();
71
- } else if ($("#node-config-input-ttsservice").val() === "googletts") {
72
- $("#microsoftAzureForm").hide();
73
- $("#pollyForm").hide();
74
- $("#elevenlabsForm").hide();
75
- $("#GoogleForm").show();
76
- } else if ($("#node-config-input-ttsservice").val() === "googletranslate") {
77
- $("#pollyForm").hide();
78
- $("#GoogleForm").hide();
79
- $("#microsoftAzureForm").hide();
80
- $("#elevenlabsForm").hide();
81
- } else if ($("#node-config-input-ttsservice").val() === "microsoftazuretts") {
82
- $("#pollyForm").hide();
83
- $("#GoogleForm").hide();
84
- $("#elevenlabsForm").hide();
85
- $("#microsoftAzureForm").show();
86
- } else if ($("#node-config-input-ttsservice").val().includes("elevenlabs")) {
87
- $("#pollyForm").hide();
88
- $("#GoogleForm").hide();
89
- $("#microsoftAzureForm").hide();
90
- $("#elevenlabsForm").show();
91
- }
92
- });
93
- // ##########################################################
60
+ // 22/12/2020 Hide parts of the ui
61
+ // ##########################################################
62
+ $("#node-config-input-ttsservice").change(function (e) {
63
+ if ($("#node-config-input-ttsservice").val() === "googletts") {
64
+ $("#elevenlabsForm").hide();
65
+ $("#voiceaiForm").hide();
66
+ $("#GoogleForm").show();
67
+ } else if ($("#node-config-input-ttsservice").val() === "googletranslate") {
68
+ $("#GoogleForm").hide();
69
+ $("#elevenlabsForm").hide();
70
+ $("#voiceaiForm").hide();
71
+ } else if ($("#node-config-input-ttsservice").val().includes("elevenlabs")) {
72
+ $("#GoogleForm").hide();
73
+ $("#voiceaiForm").hide();
74
+ $("#elevenlabsForm").show();
75
+ } else if ($("#node-config-input-ttsservice").val() === "voiceai") {
76
+ $("#GoogleForm").hide();
77
+ $("#elevenlabsForm").hide();
78
+ $("#voiceaiForm").show();
79
+ }
80
+ });
81
+ $("#node-config-input-ttsservice").trigger("change");
82
+ // ##########################################################
94
83
 
95
84
  // 022/12/2020 Upload file or files
96
85
  // ##########################################################
@@ -177,56 +166,41 @@
177
166
  </div>
178
167
  <br/>
179
168
  <p><b>SPEECH CONFIGURATION</b></p>
180
- <div class="form-row">
181
- <label for="node-config-input-ttsservice"><i class="fa fa-comments"></i> TTS Service</label>
182
- <select id="node-config-input-ttsservice">
183
- <option value="polly">(REMOVED. SEE THE GITHUB PROJECT PAGE)Amazon Polly</option>
184
- <option value="googletts">Google TTS</option>
185
- <option value="googletranslate">Google free TTS</option>
186
- <option value="microsoftazuretts">(REMOVED. SEE THE GITHUB PROJECT PAGE)Microsoft Azure TTS</option>
187
- <option value="elevenlabs">ElevenLabs TTS V1 (deprecated)</option>
188
- <option value="elevenlabsv2">ElevenLabs TTS V2 Multilingual</option>
189
- </select>&nbsp&nbsp<b><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 configure</u></a></span>
190
- </div>
191
- <div id="pollyForm">
192
- <div class="form-row">
193
- <label for="node-config-input-accessKey"><i class="fa fa-user"></i> AWS Access key</label>
194
- <input type="text" id="node-config-input-accessKey">
195
- </div>
196
- <div class="form-row">
197
- <label for="node-config-input-secretKey"><i class="fa fa-user"></i> AWS Secret key</label>
198
- <input type="password" id="node-config-input-secretKey">
199
- </div>
200
- </div>
201
- <div id="GoogleForm">
202
- <div class="form-row">
203
- <label><i class="fa fa-upload"></i> Google credentials file path</label>
204
- <input style="width:180px" id="googleCredentialsPath" type="file">
205
- </div>
206
- </div>
207
- <div id="microsoftAzureForm">
208
- <div class="form-row">
209
- <label style="width:35%" for="node-config-input-mssubscriptionKey"><i class="fa fa-user"></i> Azure subscription key</label>
210
- <input style="width:58%" type="text" id="node-config-input-mssubscriptionKey">
211
- </div>
212
- <div class="form-row">
213
- <label style="width:35%" for="node-config-input-mslocation"><i class="fa fa-user"></i> Azure location (ex:westeurope)</label>
214
- <input style="width:58%" type="text" id="node-config-input-mslocation">
215
- </div>
216
- </div>
217
- <div id="elevenlabsForm">
218
- <div class="form-row">
219
- <label style="width:35%" for="node-config-input-elevenlabsKey"><i class="fa fa-user"></i> <a href="https://elevenlabs.io" target="_new">Elevenlabs</a>API key</label>
220
- <input style="width:58%" type="text" id="node-config-input-elevenlabsKey">
221
- </div>
222
- </div>
223
- <div class="form-row">
224
- <label for="node-config-input-purgediratrestart"><i class="fa fa-folder-o"></i> TTS Cache</label>
225
- <select id="node-config-input-purgediratrestart">
226
- <option value="purge">Purge and delete this config node's TTS cache at deploy or restart(default)</option>
227
- <option value="leave">Leave the TTS cache folder untouched (not suggested if you have less disk space)</option>
228
- </select>
229
- </div>
169
+ <div class="form-row">
170
+ <label for="node-config-input-ttsservice"><i class="fa fa-comments"></i> TTS Service</label>
171
+ <select id="node-config-input-ttsservice">
172
+ <option value="googletts">Google TTS</option>
173
+ <option value="googletranslate">Google free TTS</option>
174
+ <option value="elevenlabs">ElevenLabs TTS V1 (deprecated)</option>
175
+ <option value="elevenlabsv2">ElevenLabs TTS V2 Multilingual</option>
176
+ <option value="voiceai">Voice.ai TTS</option>
177
+ </select>&nbsp&nbsp<b><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 configure</u></a></span>
178
+ </div>
179
+ <div id="GoogleForm">
180
+ <div class="form-row">
181
+ <label><i class="fa fa-upload"></i> Google credentials file path</label>
182
+ <input style="width:180px" id="googleCredentialsPath" type="file">
183
+ </div>
184
+ </div>
185
+ <div id="elevenlabsForm">
186
+ <div class="form-row">
187
+ <label style="width:35%" for="node-config-input-elevenlabsKey"><i class="fa fa-user"></i> <a href="https://elevenlabs.io" target="_new">Elevenlabs</a>API key</label>
188
+ <input style="width:58%" type="text" id="node-config-input-elevenlabsKey">
189
+ </div>
190
+ </div>
191
+ <div id="voiceaiForm">
192
+ <div class="form-row">
193
+ <label style="width:35%" for="node-config-input-voiceaiKey"><i class="fa fa-user"></i> <a href="https://voice.ai" target="_new">Voice.ai</a> API key</label>
194
+ <input style="width:58%" type="text" id="node-config-input-voiceaiKey">
195
+ </div>
196
+ </div>
197
+ <div class="form-row">
198
+ <label for="node-config-input-purgediratrestart"><i class="fa fa-folder-o"></i> TTS Cache</label>
199
+ <select id="node-config-input-purgediratrestart">
200
+ <option value="purge">Purge and delete this config node's TTS cache at deploy or restart(default)</option>
201
+ <option value="leave">Leave the TTS cache folder untouched (not suggested if you have less disk space)</option>
202
+ </select>
203
+ </div>
230
204
 
231
205
  <div class="form-row">
232
206
  <label for="node-config-input-TTSRootFolderPath"><i class="fa fa-folder-o"></i> Cache root folder</label>
@@ -247,24 +221,15 @@ IF YOU RUN NODE-RED BEHIND DOCKER OR SOMETHING ELSE, BE AWARE: <br/>
247
221
  PORT USED BY THE NODE ARE 1980 (DEFAULT) AND 1400 (FOR SONOS DISCOVER). <br/>
248
222
  PLEASE ALLOW MDNS AND UDP AS WELL
249
223
 
250
- **TTS Service**<br/>
251
- You can choose between Google (without credentials), Amazon AWS (Polly), Google TTS (require credentials and registration to google) or Microsoft Azure TTS engines.<br/>
252
- For Google TTS Engine, you can choose pitch and speed rate of the voice.
253
- <br/>
254
- <br/>
255
-
256
- * **(REMOVED AND NOT USED ANYMORE) TTS Service using Amazon AWS (Polly)**
257
-
258
- !IF YOU NEED THIS SERVICE, INSTALL ANY VERSION < 3.0.0 (ANY 2.x.x IS FINE)!
259
- > ``` npm install node-red-contrib-tts-ultimate@2.0.10 ```
224
+ **TTS Service**<br/>
225
+ You can choose between Google (without credentials), Google TTS (require credentials and registration to google), ElevenLabs or Voice.ai TTS engines.<br/>
226
+ For Google TTS Engine, you can choose pitch and speed rate of the voice.
227
+ <br/>
228
+ <br/>
260
229
 
261
- > HOW-TO in Deutsch: for german users, there is a very helpful how-to, where you can learn how to use the node and how to register to Amazon AWS Polly as well: here: https://technikkram.net/blog/2020/09/26/sonos-sprachausgabe-mit-raspberry-pi-node-red-und-amazon-polly-fuer-homematic-oder-knx-systeme
262
-
263
- <br/>
264
-
265
- * **TTS Service using Google (without credentials)**
230
+ * **TTS Service using Google (without credentials)**
266
231
 
267
- 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.
232
+ 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.
268
233
 
269
234
  <br/>
270
235
 
@@ -275,22 +240,21 @@ For Google TTS Engine, you can choose pitch and speed rate of the voice.
275
240
  Here you must select your credential file, previously downloaded from Google, [with these steps](https://www.npmjs.com/package/@google-cloud/text-to-speech):
276
241
  > [Select or create a Cloud Platform project](https://console.cloud.google.com/project)<br/>
277
242
  > [Enable billing for your project](https://support.google.com/cloud/answer/6293499#enable-billing)<br/>
278
- > [Enable the Google Cloud Text-to-Speech API](https://console.cloud.google.com/flows/enableapi?apiid=texttospeech.googleapis.com)<br/>
279
-
280
- <br/>
243
+ > [Enable the Google Cloud Text-to-Speech API](https://console.cloud.google.com/flows/enableapi?apiid=texttospeech.googleapis.com)<br/>
244
+
245
+ <br/>
281
246
 
282
- * **(REMOVED AND NOT USED ANYMORE) TTS Service using Microsot Azure TTS**
247
+ * **TTS Service using ElevenLabs**
283
248
 
284
- !IF YOU NEED THIS SERVICE, INSTALL ANY VERSION < 3.0.0 (ANY 2.x.x IS FINE)!
285
- > ``` npm install node-red-contrib-tts-ultimate@2.0.10 ```
286
-
249
+ 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.
250
+ 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.
251
+ 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/>
287
252
  <br/>
288
253
 
289
- * **TTS Service using ElevenLabs**
254
+ * **TTS Service using Voice.ai**
290
255
 
291
- 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.
292
- 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.
293
- 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/>
256
+ 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.
257
+ Note: SSML is not supported by this engine.
294
258
  <br/>
295
259
 
296
260
 
@@ -21,6 +21,8 @@ module.exports = function (RED) {
21
21
  const oOS = require('os');
22
22
  const sonos = require('sonos');
23
23
 
24
+ const VOICEAI_API_BASE_URL = "https://dev.voice.ai/api/v1/tts";
25
+
24
26
  // Configuration Node Register
25
27
  function TTSConfigNode(config) {
26
28
  RED.nodes.createNode(this, config);
@@ -209,6 +211,52 @@ module.exports = function (RED) {
209
211
  //RED.log.info("ttsultimate-config " + node.id + ": Google Translate free service not used.");
210
212
  }
211
213
 
214
+ // Voice.ai TTS
215
+ if (node.ttsservice === "voiceai") {
216
+ node.voiceAiVoiceList = [];
217
+
218
+ const loadVoiceAiVoices = async () => {
219
+ try {
220
+ const apiKey = node.credentials.voiceaiKey;
221
+ if (!apiKey || apiKey.trim() === "") {
222
+ node.voiceAiVoiceList = [{ name: "Voice.ai API key missing. Please configure, deploy and restart node-red.", id: "voiceai_default" }];
223
+ return;
224
+ }
225
+
226
+ const res = await fetch(`${VOICEAI_API_BASE_URL}/voices`, {
227
+ method: "GET",
228
+ headers: { Authorization: `Bearer ${apiKey}` }
229
+ });
230
+
231
+ if (!res.ok) {
232
+ const body = await res.text().catch(() => "");
233
+ throw new Error(`HTTP ${res.status} ${res.statusText}${body ? " - " + body : ""}`);
234
+ }
235
+
236
+ const payload = await res.json();
237
+ const voices = Array.isArray(payload) ? payload : [];
238
+
239
+ const list = [{ name: "Default (built-in)", id: "voiceai_default" }];
240
+ const voiceEntries = [];
241
+ voices.forEach(v => {
242
+ if (v && v.voice_id) {
243
+ const extra = [v.voice_visibility, v.status].filter(Boolean).join(", ");
244
+ voiceEntries.push({ name: `${v.name || v.voice_id}${extra ? " (" + extra + ")" : ""}`, id: v.voice_id });
245
+ }
246
+ });
247
+ voiceEntries.sort((a, b) => (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase()));
248
+ node.voiceAiVoiceList = list.concat(voiceEntries);
249
+ RED.log.info("ttsultimate-config " + node.id + ": Voice.ai TTS enabled.");
250
+ } catch (error) {
251
+ RED.log.warn("ttsultimate-config " + node.id + ": Voice.ai TTS disabled. " + error.message);
252
+ node.voiceAiVoiceList = [{ name: "Error getting Voice.ai voices: " + error.message + " Check credentials, deploy and restart node-red.", id: "voiceai_default" }];
253
+ }
254
+ };
255
+
256
+ // Fire and forget; list will be available shortly after restart/deploy
257
+ loadVoiceAiVoices();
258
+ }
259
+
212
260
 
213
261
  //#endregion
214
262
 
@@ -416,6 +464,43 @@ module.exports = function (RED) {
416
464
 
417
465
  } else if (ttsservice.includes("elevenlabs")) {
418
466
  res.json(node.elevenlabsTTSVoiceList);
467
+ } else if (ttsservice === "voiceai") {
468
+ const ensureList = async () => {
469
+ try {
470
+ if (Array.isArray(node.voiceAiVoiceList) && node.voiceAiVoiceList.length > 0) {
471
+ return node.voiceAiVoiceList;
472
+ }
473
+ const apiKey = node.credentials.voiceaiKey;
474
+ if (!apiKey || apiKey.trim() === "") {
475
+ return [{ name: "Voice.ai API key missing. Please configure, deploy and restart node-red.", id: "voiceai_default" }];
476
+ }
477
+
478
+ const resVoices = await fetch(`${VOICEAI_API_BASE_URL}/voices`, {
479
+ method: "GET",
480
+ headers: { Authorization: `Bearer ${apiKey}` }
481
+ });
482
+ if (!resVoices.ok) {
483
+ const body = await resVoices.text().catch(() => "");
484
+ throw new Error(`HTTP ${resVoices.status} ${resVoices.statusText}${body ? " - " + body : ""}`);
485
+ }
486
+ const payload = await resVoices.json();
487
+ const voices = Array.isArray(payload) ? payload : [];
488
+ const list = [{ name: "Default (built-in)", id: "voiceai_default" }];
489
+ const voiceEntries = [];
490
+ voices.forEach(v => {
491
+ if (v && v.voice_id) {
492
+ const extra = [v.voice_visibility, v.status].filter(Boolean).join(", ");
493
+ voiceEntries.push({ name: `${v.name || v.voice_id}${extra ? " (" + extra + ")" : ""}`, id: v.voice_id });
494
+ }
495
+ });
496
+ voiceEntries.sort((a, b) => (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase()));
497
+ node.voiceAiVoiceList = list.concat(voiceEntries);
498
+ return node.voiceAiVoiceList;
499
+ } catch (error) {
500
+ return [{ name: "Error getting Voice.ai voices: " + error.message + " Check credentials, deploy and restart node-red.", id: "voiceai_default" }];
501
+ }
502
+ };
503
+ ensureList().then(list => res.json(list));
419
504
  }
420
505
 
421
506
  });
@@ -642,11 +727,8 @@ module.exports = function (RED) {
642
727
  }
643
728
  RED.nodes.registerType("ttsultimate-config", TTSConfigNode, {
644
729
  credentials: {
645
- accessKey: { type: "text" },
646
- secretKey: { type: "password" },
647
- mssubscriptionKey: { type: "text" },
648
- mslocation: { type: "text" },
649
- elevenlabsKey: { type: "text" }
730
+ elevenlabsKey: { type: "text" },
731
+ voiceaiKey: { type: "text" }
650
732
  }
651
733
  });
652
734
 
@@ -226,35 +226,48 @@
226
226
  label: function () {
227
227
  return this.name || "TTS-ultimate";
228
228
  },
229
- oneditprepare: function () {
230
- var node = this;
231
- var oNodeServer = RED.nodes.node($("#node-input-config").val()); // Store the config-node
232
- if (node.elevenlabsModel !== undefined) $("#node-input-elevenlabsModel").val(node.elevenlabsModel);
233
- if (node.elevenlabsOptimizeLatency !== undefined) $("#node-input-elevenlabsOptimizeLatency").val(node.elevenlabsOptimizeLatency);
234
- if (node.elevenlabsOutputFormat !== undefined) $("#node-input-elevenlabsOutputFormat").val(node.elevenlabsOutputFormat);
235
- if (node.elevenlabsSeed !== undefined) $("#node-input-elevenlabsSeed").val(node.elevenlabsSeed);
229
+ oneditprepare: function () {
230
+ var node = this;
231
+ var oNodeServer = RED.nodes.node($("#node-input-config").val()); // Store the config-node
232
+ if (node.elevenlabsModel !== undefined) $("#node-input-elevenlabsModel").val(node.elevenlabsModel);
233
+ if (node.elevenlabsOptimizeLatency !== undefined) $("#node-input-elevenlabsOptimizeLatency").val(node.elevenlabsOptimizeLatency);
234
+ if (node.elevenlabsOutputFormat !== undefined) $("#node-input-elevenlabsOutputFormat").val(node.elevenlabsOutputFormat);
235
+ if (node.elevenlabsSeed !== undefined) $("#node-input-elevenlabsSeed").val(node.elevenlabsSeed);
236
+
237
+ function updateTtsOptionsVisibility() {
238
+ try {
239
+ if (!oNodeServer || !oNodeServer.ttsservice) return;
240
+ if (oNodeServer.ttsservice === "googletts") {
241
+ $("#divGoogleTTSAudioConfig").show();
242
+ $("#divElevenLabsOptions").hide();
243
+ $("#divSSML").show();
244
+ } else if (oNodeServer.ttsservice.includes("elevenlabs")) {
245
+ $("#divGoogleTTSAudioConfig").hide();
246
+ $("#divElevenLabsOptions").show();
247
+ $("#divSSML").hide();
248
+ } else if (oNodeServer.ttsservice === "voiceai") {
249
+ $("#divGoogleTTSAudioConfig").hide();
250
+ $("#divElevenLabsOptions").hide();
251
+ $("#divSSML").hide();
252
+ } else {
253
+ $("#divGoogleTTSAudioConfig").hide();
254
+ $("#divElevenLabsOptions").hide();
255
+ $("#divSSML").show();
256
+ }
257
+ } catch (error) {
258
+ }
259
+ }
236
260
 
237
261
  // 19/02/2020 Used to alert the user if the CSV file has not been loaded and to get the server sooner als deploy
238
262
  // ###########################
239
- $("#node-input-config").change(function () {
240
- try {
241
- oNodeServer = RED.nodes.node($(this).val());
242
- getVoices();
243
- if (oNodeServer.ttsservice === "googletts") {
244
- $("#divGoogleTTSAudioConfig").show();
245
- $("#divElevenLabsOptions").hide();
246
- } else if (oNodeServer.ttsservice.includes("elevenlabs")) {
247
- $("#divGoogleTTSAudioConfig").hide();
248
- $("#divElevenLabsOptions").show();
249
- $("#divSSML").hide();
250
- } else {
251
- $("#divGoogleTTSAudioConfig").hide();
252
- $("#divElevenLabsOptions").hide();
253
- $("#divSSML").show();
254
- }
255
- } catch (error) {
256
- }
257
- });
263
+ $("#node-input-config").change(function () {
264
+ try {
265
+ oNodeServer = RED.nodes.node($(this).val());
266
+ getVoices();
267
+ updateTtsOptionsVisibility();
268
+ } catch (error) {
269
+ }
270
+ });
258
271
  // ###########################
259
272
 
260
273
 
@@ -347,9 +360,10 @@
347
360
  }
348
361
  return comparison;
349
362
  }
350
- }
351
- getVoices();
352
- // #####################################
363
+ }
364
+ updateTtsOptionsVisibility();
365
+ getVoices();
366
+ // #####################################
353
367
 
354
368
  // Refresh the combo
355
369
  // #####################################
@@ -601,7 +615,7 @@
601
615
  |Property|Description|
602
616
  |--|--|
603
617
  | TTS Service | Select the TTS SERVICE ENGINE NODE. |
604
- | Voice | Select your preferred voice. If you use Amazon, Polly voices will be displayed (standard and neural). If you use Google, google voices will be displayed. Google service without authentication, has a limited set of voices. |
618
+ | Voice | Select your preferred voice. The list depends on the selected TTS engine (Google / ElevenLabs / Voice.ai). Google service without authentication has a limited set of voices. |
605
619
  | Hailing | Before the first TTS message of the message queues, the node will play an "hailing" sound. You can select the hailing, upload your own, or totally disable it. |
606
620
  | Upload hail | It allows you to upload your own hailing file. |
607
621
  | Player | Select the player. If you select not to use a player, the node will output a msg with an array of files, ready to be played by third party nodes. In case you select No player, only output file name, you'll get a message with an additional property filesArray, containing an array of all mp3 files ready to be played with third party nodes. Please see below the OUTPUT MESSAGES FROM THE NODE section. |
@@ -614,7 +628,7 @@
614
628
  **Google TTS Engines specific options**
615
629
  |Property|Description|
616
630
  |--|--|
617
- | Enable SSML | Enable the SSML XML notation. Please be aware, not all the TTS engines supports that.|
631
+ | Enable SSML | Enable the SSML XML notation. Please be aware, not all the TTS engines supports that (for example Voice.ai).|
618
632
  | Rate | Only avaiable if you choose Google TTS Engine (with credentials). Specifies the speech speed (Between 0.25 and 4.0, default 1). |
619
633
  | Pitch | Only avaiable if you choose Google TTS Engine (with credentials). Specifies the speech pitch (Between -20.0 and 20.0, default 0). |
620
634
 
@@ -637,7 +651,7 @@
637
651
  : sonoshailing (string) : Overrides the selected hailing and plays the filename you passed in. Please double check the spelling of the filename (must be the same as you can see in the dropdown list of your hailing files, in the ttsultimate config window) and do not include the .mp3 extenson. For example node.sonoshailing="ComputerCall".
638
652
  : priority (boolean) : If set to true, the inbound flow message will cancel the current TTS queue, will stop the current phrase being spoken and the node will play this priority message. If there are other priority messages in the queue, they will be retained and the inbound priority flow message is added to the queue. If the inbound priority flow message is the first in the priority queue, the hailing is played first (if the hailing has been enabled or if the hailing has been overridden by node.sonoshailing).
639
653
  : stop (boolean) : If set to true, stops whatever is playing and clears the TTS queue.
640
- : voiceId (number) : Play a message with custom voice ID.
654
+ : voiceId (string) : Play a message with custom voice ID (engine-specific).
641
655
  : setConfig (json) : This is the property where you can set all the things. It must be a JSON Object with the below specified properties. The setting is retained until the node receives another msg.setConfig or until node-red is restarted.
642
656
  : setConfig {setMainPlayerIP} (string) : Sets the main player IP. This will also be the coordinator if you have a group of players.
643
657
  : setConfig {setPlayerGroupArray} (array) : Sets the array of players beloging to the group, if any. You can also specify the volume variation from the main volume player, to adapt the additional player's perceived volume to the main sonos player volume. For example, if you have a speaker mounted in celiling, having less perceived volume, you can "push" the volume up, to match the whole perceived volume. Just add # after the IP and a number from -100 to 100 to subtract or add volume compared to the main sonos volume. For example, if the sonos main player volume is 40, you can push this celing speaker's volume to further 10, so it'll have the real volume of 50. See below, the example. Even if you have only one additional player, you need to put it into an array.
@@ -7,6 +7,7 @@ module.exports = function (RED) {
7
7
  var path = require('path');
8
8
  const sonos = require('sonos');
9
9
  const crypto = require("crypto");
10
+ const VOICEAI_API_BASE_URL = "https://dev.voice.ai/api/v1/tts";
10
11
 
11
12
  function slug(_text) {
12
13
  var sRet = _text;
@@ -675,6 +676,28 @@ module.exports = function (RED) {
675
676
  } else {
676
677
  node.setNodeStatus({ fill: 'green', shape: 'ring', text: 'Reading offline from cache' });
677
678
  }
679
+ } else if (node.server.ttsservice === "voiceai") {
680
+ const apiKey = node.server.credentials.voiceaiKey;
681
+ if (!apiKey || apiKey.trim() === "") throw new Error("Voice.ai API key missing");
682
+
683
+ const params = {
684
+ text: msg,
685
+ audio_format: "mp3"
686
+ };
687
+ if (node.voiceId && node.voiceId !== "" && node.voiceId !== "voiceai_default") {
688
+ params.voice_id = node.voiceId;
689
+ }
690
+
691
+ const cacheFilename = getFilename(msg, params);
692
+ const { isCached, resolvedPath } = resolveCachePath(cacheFilename);
693
+ node.sFileToBePlayed = resolvedPath;
694
+ audioSource = isCached ? "cache" : "internet";
695
+ if (!isCached) {
696
+ node.setNodeStatus({ fill: 'blue', shape: 'ring', text: 'Download using ' + node.server.ttsservice });
697
+ data = await synthesizeSpeechVoiceAi(apiKey, params);
698
+ } else {
699
+ node.setNodeStatus({ fill: 'green', shape: 'ring', text: 'Reading offline from cache' });
700
+ }
678
701
  } else if (node.server.ttsservice === "microsoftazuretts") {
679
702
  // VoiceId is: code
680
703
  const params = {
@@ -1328,6 +1351,29 @@ module.exports = function (RED) {
1328
1351
  }
1329
1352
  }
1330
1353
 
1354
+ async function synthesizeSpeechVoiceAi(apiKey, params) {
1355
+ try {
1356
+ const res = await fetch(`${VOICEAI_API_BASE_URL}/speech`, {
1357
+ method: "POST",
1358
+ headers: {
1359
+ Authorization: `Bearer ${apiKey}`,
1360
+ "Content-Type": "application/json"
1361
+ },
1362
+ body: JSON.stringify(params)
1363
+ });
1364
+
1365
+ if (!res.ok) {
1366
+ const body = await res.text().catch(() => "");
1367
+ throw new Error(`HTTP ${res.status} ${res.statusText}${body ? " - " + body : ""}`);
1368
+ }
1369
+
1370
+ const ab = await res.arrayBuffer();
1371
+ return Buffer.from(ab);
1372
+ } catch (error) {
1373
+ throw (error);
1374
+ }
1375
+ }
1376
+
1331
1377
  // 04/01/2021 hashing filename to avoid issues with long filenames.
1332
1378
  function getFilename(_text, _params) {
1333
1379
  let sTextToBeHashed = _text.concat(JSON.stringify(_params));