node-red-contrib-tts-ultimate 3.0.0 → 3.0.2
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 +7 -2
- package/package.json +1 -1
- package/ttsultimate/ttsultimate.html +61 -2
- package/ttsultimate/ttsultimate.js +57 -38
package/CHANGELOG.md
CHANGED
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.paypal.me/techtoday)
|
|
4
4
|
|
|
5
|
-
## BREAKING CHANGE ! BREAKING CHANGE ! BREAKING CHANGE ! BREAKING CHANGE !
|
|
6
5
|
|
|
7
6
|
<p>
|
|
8
|
-
<b>Version 3.0.
|
|
7
|
+
<b>Version 3.0.1</b> October 2025<br/>
|
|
8
|
+
- Elevenlabs Engine: added more option to personalize the voice.<br/>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p>
|
|
12
|
+
<b>Version 3.0.0</b> June 2025<br/>
|
|
9
13
|
- 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/>
|
|
14
|
+
- NEW: Added option to avoid resuming music if it was playing before TTS messages.<br/>
|
|
10
15
|
</p>
|
|
11
16
|
|
|
12
17
|
-----------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-tts-ultimate",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
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.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -49,6 +49,47 @@
|
|
|
49
49
|
<label></label>
|
|
50
50
|
<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
51
|
</div>
|
|
52
|
+
<div class="form-row">
|
|
53
|
+
<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>
|
|
56
|
+
<option value="eleven_monolingual_v1">Eleven Monolingual v1</option>
|
|
57
|
+
<option value="eleven_multilingual_v1">Eleven Multilingual v1</option>
|
|
58
|
+
<option value="eleven_multilingual_v2">Eleven Multilingual v2</option>
|
|
59
|
+
<option value="eleven_turbo_v2">Eleven Turbo v2</option>
|
|
60
|
+
<option value="eleven_turbo_v2_5">Eleven Turbo v2.5</option>
|
|
61
|
+
</select>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="form-row">
|
|
64
|
+
<label for="node-input-elevenlabsOptimizeLatency"><i class="fa fa-tachometer"></i> Latency preset</label>
|
|
65
|
+
<select id="node-input-elevenlabsOptimizeLatency" style="width:60%">
|
|
66
|
+
<option value="">Default</option>
|
|
67
|
+
<option value="0">0 - Best quality</option>
|
|
68
|
+
<option value="1">1</option>
|
|
69
|
+
<option value="2">2</option>
|
|
70
|
+
<option value="3">3</option>
|
|
71
|
+
<option value="4">4 - Lowest latency</option>
|
|
72
|
+
</select>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="form-row">
|
|
75
|
+
<label for="node-input-elevenlabsOutputFormat"><i class="fa fa-file-audio-o"></i> Output format</label>
|
|
76
|
+
<select id="node-input-elevenlabsOutputFormat" style="width:60%">
|
|
77
|
+
<option value="">Default (mp3_44100_128)</option>
|
|
78
|
+
<option value="mp3_44100_192">mp3_44100_192</option>
|
|
79
|
+
<option value="mp3_44100_128">mp3_44100_128</option>
|
|
80
|
+
<option value="mp3_44100_64">mp3_44100_64</option>
|
|
81
|
+
<option value="mp3_44100_32">mp3_44100_32</option>
|
|
82
|
+
<option value="mp3_22050_128">mp3_22050_128</option>
|
|
83
|
+
<option value="mp3_22050_64">mp3_22050_64</option>
|
|
84
|
+
<option value="pcm_16000">pcm_16000</option>
|
|
85
|
+
<option value="pcm_22050">pcm_22050</option>
|
|
86
|
+
<option value="ulaw_8000">ulaw_8000</option>
|
|
87
|
+
</select>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="form-row">
|
|
90
|
+
<label for="node-input-elevenlabsSeed"><i class="fa fa-random"></i> Seed</label>
|
|
91
|
+
<input type="text" id="node-input-elevenlabsSeed" style="width:120px" placeholder="Leave blank for random">
|
|
92
|
+
</div>
|
|
52
93
|
</div>
|
|
53
94
|
<div class="form-row" id="divSSML">
|
|
54
95
|
<label></label>
|
|
@@ -79,7 +120,10 @@
|
|
|
79
120
|
<label for="node-input-unmuteIfMuted"><i class="fa fa-bell-slash-o"></i> Unmute</label>
|
|
80
121
|
<input type="checkbox" id="node-input-unmuteIfMuted" style="margin-left: 0px; vertical-align: top; width: auto !important;"> <label style="width:auto !important;"> Unmute, then restore previous state after play.</label>
|
|
81
122
|
</div>
|
|
82
|
-
|
|
123
|
+
<div class="form-row">
|
|
124
|
+
<label for="node-input-doNotResumeMusic"><i class="fa fa-bell-slash-o"></i> Resume</label>
|
|
125
|
+
<input type="checkbox" id="node-input-doNotResumeMusic" style="margin-left: 0px; vertical-align: top; width: auto !important;"> <label style="width:auto !important;"> Do not resume previous music.</label>
|
|
126
|
+
</div>
|
|
83
127
|
<div class="form-row">
|
|
84
128
|
<label for="node-input-sonosipaddress"><i class="fa fa-globe"></i> Main Sonos Player</label>
|
|
85
129
|
<label style="width:200px;" id="node-input-sonosipaddress">Discovering.... wait...</label>
|
|
@@ -136,6 +180,12 @@
|
|
|
136
180
|
value: "Hailing_Hailing.mp3",
|
|
137
181
|
required: false,
|
|
138
182
|
},
|
|
183
|
+
doNotResumeMusic:
|
|
184
|
+
{
|
|
185
|
+
value: true,
|
|
186
|
+
required: false,
|
|
187
|
+
},
|
|
188
|
+
|
|
139
189
|
config:
|
|
140
190
|
{
|
|
141
191
|
type: "ttsultimate-config",
|
|
@@ -152,6 +202,10 @@
|
|
|
152
202
|
elevenlabsSimilarity_boost: { value: "0.5", required: false },
|
|
153
203
|
elevenlabsStyle: { value: "0.0", required: false },
|
|
154
204
|
elevenlabsUse_speaker_boost: { value: true, required: false },
|
|
205
|
+
elevenlabsModel: { value: "", required: false },
|
|
206
|
+
elevenlabsOptimizeLatency: { value: "", required: false },
|
|
207
|
+
elevenlabsOutputFormat: { value: "", required: false },
|
|
208
|
+
elevenlabsSeed: { value: "", required: false },
|
|
155
209
|
},
|
|
156
210
|
inputs: 1,
|
|
157
211
|
outputs: 2,
|
|
@@ -175,6 +229,10 @@
|
|
|
175
229
|
oneditprepare: function () {
|
|
176
230
|
var node = this;
|
|
177
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);
|
|
178
236
|
|
|
179
237
|
// 19/02/2020 Used to alert the user if the CSV file has not been loaded and to get the server sooner als deploy
|
|
180
238
|
// ###########################
|
|
@@ -549,6 +607,7 @@
|
|
|
549
607
|
| 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. |
|
|
550
608
|
| Volume | Set the preferred TTS volume, from "0" to "100" (can be overridden by passing msg.volume = "40"; to the node). |
|
|
551
609
|
| Unmute | Unmute the main and the addotional players, then restore the previous mute state once finished. (Can be overridden by passing msg.unmute = true; to the node). |
|
|
610
|
+
| Resume | If music was playing prior to TTS messages, the node will try to resume it, but can fail in some cases. Enabla this option to avoid resuming music after TTS message. |
|
|
552
611
|
| Main Sonos Player | Select your Sonos primary player. (It's strongly suggested to set a fixed IP for this player; you can reserve an IP using the DHCP Reservation function of your router/firewall's DHCP Server). It's possibile to group players, so your announcement can be played on all selected players. For this to happen, you need to select your primary coordinator player. All other players will be then controlled by this coordinator. |
|
|
553
612
|
| Additional Players | Here you can add all additional players that will be grouped toghether to the Main Sonos Player coordinator group. You can add a player using the "ADD" button, below the list. For each additional player, you can adjust their volume, based on the Main Sonos Player volume -+100. |
|
|
554
613
|
|
|
@@ -602,4 +661,4 @@ The node has two output pins. The first pin is to signal play status, the second
|
|
|
602
661
|
[Find it useful?](https://www.paypal.me/techtoday)
|
|
603
662
|
|
|
604
663
|
<br/>
|
|
605
|
-
</script>
|
|
664
|
+
</script>
|
|
@@ -80,7 +80,7 @@ module.exports = function (RED) {
|
|
|
80
80
|
node.sonosCoordinatorIsPreviouslyMuted = false;
|
|
81
81
|
node.passThroughMessage = {};
|
|
82
82
|
node.bTimeOutPlay = false;
|
|
83
|
-
|
|
83
|
+
node.doNotResumeMusic = config.doNotResumeMusic === undefined ? false : config.doNotResumeMusic; // 06/2025 Do not resume previous music after TTS, if playing.
|
|
84
84
|
if (typeof node.server !== "undefined" && node.server !== null) {
|
|
85
85
|
node.sNoderedURL = node.server.sNoderedURL || "";
|
|
86
86
|
}
|
|
@@ -660,15 +660,18 @@ module.exports = function (RED) {
|
|
|
660
660
|
}
|
|
661
661
|
} else if (node.server.ttsservice === "elevenlabs") {
|
|
662
662
|
// VoiceId is: code
|
|
663
|
+
const stability = config.elevenlabsStability !== undefined && config.elevenlabsStability !== "" ? Number(config.elevenlabsStability) : undefined;
|
|
664
|
+
const similarity = config.elevenlabsSimilarity_boost !== undefined && config.elevenlabsSimilarity_boost !== "" ? Number(config.elevenlabsSimilarity_boost) : undefined;
|
|
665
|
+
const resolvedModel = config.elevenlabsModel && config.elevenlabsModel !== "" ? config.elevenlabsModel : "eleven_monolingual_v1";
|
|
663
666
|
const params = {
|
|
664
667
|
text: msg,
|
|
665
668
|
voice: node.voiceId,
|
|
666
|
-
model_id:
|
|
667
|
-
voice_settings: {
|
|
668
|
-
stability: config.elevenlabsStability,
|
|
669
|
-
similarity_boost: config.elevenlabsSimilarity_boost
|
|
670
|
-
}
|
|
669
|
+
model_id: resolvedModel,
|
|
670
|
+
voice_settings: {}
|
|
671
671
|
};
|
|
672
|
+
if (stability !== undefined && !Number.isNaN(stability)) params.voice_settings.stability = stability;
|
|
673
|
+
if (similarity !== undefined && !Number.isNaN(similarity)) params.voice_settings.similarity_boost = similarity;
|
|
674
|
+
if (Object.keys(params.voice_settings).length === 0) delete params.voice_settings;
|
|
672
675
|
// Download or read from cache
|
|
673
676
|
node.sFileToBePlayed = getFilename(msg, params);
|
|
674
677
|
node.sFileToBePlayed = path.join(node.userDir, "ttsfiles", node.sFileToBePlayed);
|
|
@@ -680,18 +683,29 @@ module.exports = function (RED) {
|
|
|
680
683
|
}
|
|
681
684
|
} else if (node.server.ttsservice === "elevenlabsv2") {
|
|
682
685
|
// VoiceId is: code
|
|
686
|
+
const stability = config.elevenlabsStability !== undefined && config.elevenlabsStability !== "" ? Number(config.elevenlabsStability) : undefined;
|
|
687
|
+
const similarity = config.elevenlabsSimilarity_boost !== undefined && config.elevenlabsSimilarity_boost !== "" ? Number(config.elevenlabsSimilarity_boost) : undefined;
|
|
688
|
+
const style = config.elevenlabsStyle !== undefined && config.elevenlabsStyle !== "" ? Number(config.elevenlabsStyle) : undefined;
|
|
689
|
+
const resolvedModel = config.elevenlabsModel && config.elevenlabsModel !== "" ? config.elevenlabsModel : "eleven_multilingual_v2";
|
|
690
|
+
const latencyPreset = config.elevenlabsOptimizeLatency && config.elevenlabsOptimizeLatency !== "" ? config.elevenlabsOptimizeLatency : undefined;
|
|
691
|
+
const outputFormat = config.elevenlabsOutputFormat && config.elevenlabsOutputFormat !== "" ? config.elevenlabsOutputFormat : undefined;
|
|
692
|
+
const seed = config.elevenlabsSeed && config.elevenlabsSeed !== "" ? Number(config.elevenlabsSeed) : undefined;
|
|
693
|
+
const useSpeakerBoost = config.elevenlabsUse_speaker_boost === undefined ? true : config.elevenlabsUse_speaker_boost;
|
|
683
694
|
const params = {
|
|
684
695
|
stream: false,
|
|
685
696
|
text: msg,
|
|
686
697
|
voice: node.voiceId,
|
|
687
|
-
model_id:
|
|
688
|
-
voice_settings: {
|
|
689
|
-
stability: config.elevenlabsStability,
|
|
690
|
-
similarity_boost: config.elevenlabsSimilarity_boost,
|
|
691
|
-
style: config.elevenlabsStyle || 0,
|
|
692
|
-
use_speaker_boost: config.elevenlabsUse_speaker_boost === undefined ? true : config.elevenlabsUse_speaker_boost
|
|
693
|
-
}
|
|
698
|
+
model_id: resolvedModel,
|
|
699
|
+
voice_settings: {}
|
|
694
700
|
};
|
|
701
|
+
if (stability !== undefined && !Number.isNaN(stability)) params.voice_settings.stability = stability;
|
|
702
|
+
if (similarity !== undefined && !Number.isNaN(similarity)) params.voice_settings.similarity_boost = similarity;
|
|
703
|
+
if (style !== undefined && !Number.isNaN(style)) params.voice_settings.style = style;
|
|
704
|
+
params.voice_settings.use_speaker_boost = useSpeakerBoost;
|
|
705
|
+
if (Object.keys(params.voice_settings).length === 0) delete params.voice_settings;
|
|
706
|
+
if (latencyPreset !== undefined) params.optimize_streaming_latency = latencyPreset;
|
|
707
|
+
if (outputFormat !== undefined) params.output_format = outputFormat;
|
|
708
|
+
if (seed !== undefined && !Number.isNaN(seed)) params.seed = seed;
|
|
695
709
|
// Download or read from cache
|
|
696
710
|
node.sFileToBePlayed = getFilename(msg, params);
|
|
697
711
|
node.sFileToBePlayed = path.join(node.userDir, "ttsfiles", node.sFileToBePlayed);
|
|
@@ -857,37 +871,39 @@ module.exports = function (RED) {
|
|
|
857
871
|
await delay(2000);
|
|
858
872
|
|
|
859
873
|
// Resume music
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
874
|
+
if (!node.doNotResumeMusic) {
|
|
875
|
+
try {
|
|
876
|
+
if (oCurTrack !== null && (!oCurTrack.hasOwnProperty("title") || oCurTrack.title.indexOf(".mp3") === -1)) {
|
|
877
|
+
node.setNodeStatus({ fill: 'grey', shape: 'ring', text: "Resuming original queue..." });
|
|
878
|
+
await resumeMusicQueue(oCurTrack);
|
|
879
|
+
node.setNodeStatus({ fill: 'green', shape: 'ring', text: "Done resuming queue." });
|
|
880
|
+
} else {
|
|
881
|
+
// 28/08/2021 There was no queue playing. Delete the TTS from the queue
|
|
882
|
+
node.setNodeStatus({ fill: 'green', shape: 'ring', text: "No queue to resume." });
|
|
883
|
+
}
|
|
884
|
+
} catch (error) {
|
|
885
|
+
node.setNodeStatus({ fill: 'red', shape: 'ring', text: "Error resuming queue: " + error.message });
|
|
868
886
|
}
|
|
869
|
-
} catch (error) {
|
|
870
|
-
node.setNodeStatus({ fill: 'red', shape: 'ring', text: "Error resuming queue: " + error.message });
|
|
871
887
|
}
|
|
872
888
|
|
|
873
|
-
|
|
874
889
|
// 19/04/2022 Resume music queue of additional players
|
|
875
|
-
|
|
876
|
-
let
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
890
|
+
if (!node.doNotResumeMusic) {
|
|
891
|
+
for (let index = 0; index < node.oAdditionalSonosPlayers.length; index++) {
|
|
892
|
+
let addPlayer = node.oAdditionalSonosPlayers[index].oPlayer;
|
|
893
|
+
let trackAddPlayer = addPlayer.additionalPlayerCurrentTrack;
|
|
894
|
+
if (trackAddPlayer !== null) {
|
|
895
|
+
try {
|
|
896
|
+
await resumeMusicQueue(trackAddPlayer, addPlayer);
|
|
897
|
+
node.setNodeStatus({ fill: 'green', shape: 'ring', text: "Done resuming queue additional player " + addPlayer.host || "" });
|
|
898
|
+
} catch (error) {
|
|
899
|
+
// Dont care
|
|
900
|
+
RED.log.warn("ttsultimate: Error resuming music queue of additional player " + error.message + " " + addPlayer.host || "");
|
|
901
|
+
}
|
|
902
|
+
} else {
|
|
903
|
+
node.setNodeStatus({ fill: 'green', shape: 'ring', text: "No queue to resume for " + addPlayer.host || "" });
|
|
885
904
|
}
|
|
886
|
-
} else {
|
|
887
|
-
node.setNodeStatus({ fill: 'green', shape: 'ring', text: "No queue to resume for " + addPlayer.host || "" });
|
|
888
905
|
}
|
|
889
906
|
}
|
|
890
|
-
|
|
891
907
|
// Signal end playing
|
|
892
908
|
let t = setTimeout(() => {
|
|
893
909
|
node.msg.completed = true;
|
|
@@ -1192,7 +1208,10 @@ module.exports = function (RED) {
|
|
|
1192
1208
|
}
|
|
1193
1209
|
return new Promise((resolve, reject) => {
|
|
1194
1210
|
// "model_id": "eleven_multilingual_v1",
|
|
1195
|
-
|
|
1211
|
+
const stability = params.voice_settings && params.voice_settings.stability !== undefined ? params.voice_settings.stability : null;
|
|
1212
|
+
const similarity = params.voice_settings && params.voice_settings.similarity_boost !== undefined ? params.voice_settings.similarity_boost : null;
|
|
1213
|
+
const model = params.model_id !== undefined && params.model_id !== "" ? params.model_id : "eleven_monolingual_v1";
|
|
1214
|
+
ttsService.textToSpeechStream(node.server.credentials.elevenlabsKey, params.voice, params.text, stability, similarity, model).then((res) => {
|
|
1196
1215
|
try {
|
|
1197
1216
|
if (res !== undefined) {
|
|
1198
1217
|
resolve(stream2buffer(res));
|