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 +14 -0
- package/package.json +1 -1
- package/ttsultimate/ttsultimate-config.js +40 -0
- package/ttsultimate/ttsultimate.html +121 -9
- package/ttsultimate/ttsultimate.js +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
[](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.
|
|
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>    <span style="color:red"><i class="fa fa-question-circle"></i> <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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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:
|
|
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;
|