node-red-contrib-tts-ultimate 3.0.7 → 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
@@ -2,6 +2,20 @@
2
2
 
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
+ <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>
5
19
  <b>Version 3.0.7</b> March 2026<br/>
6
20
 
7
21
  - CHORE: fixed some issues with voice.ai.<br/>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-tts-ultimate",
3
- "version": "3.0.7",
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": {
@@ -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;