node-red-contrib-tts-ultimate 2.0.1 → 2.0.3
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 +9 -0
- package/package.json +1 -1
- package/ttsultimate/ttsultimate.html +21 -3
- package/ttsultimate/ttsultimate.js +121 -84
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.paypal.me/techtoday)
|
|
4
4
|
|
|
5
|
+
<p>
|
|
6
|
+
<b>Version 2.0.3</b> August 2023<br/>
|
|
7
|
+
- Fixed duplicated filenames.<br/>
|
|
8
|
+
</p>
|
|
9
|
+
<p>
|
|
10
|
+
<b>Version 2.0.2</b> August 2023<br/>
|
|
11
|
+
- NEW: added options for changing Elevenlabs voice settings.<br/>
|
|
12
|
+
- Fixed filename of the cached files, by including all settings. Previously, some settings were not taken in consideration.<br/>
|
|
13
|
+
</p>
|
|
5
14
|
<p>
|
|
6
15
|
<b>Version 2.0.1</b> August 2023<br/>
|
|
7
16
|
- NEW: added Elevenlabs TTS engine https://elevenlabs.io.<br/>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-tts-ultimate",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
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": {
|
|
@@ -32,7 +32,17 @@
|
|
|
32
32
|
<input type="text" id="node-input-speakingpitch" style="width:60px"> (Between -20.0 and 20.0, default 0)
|
|
33
33
|
</div>
|
|
34
34
|
</div>
|
|
35
|
-
<div
|
|
35
|
+
<div id="divElevenLabsOptions" hidden>
|
|
36
|
+
<div class="form-row">
|
|
37
|
+
<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)
|
|
39
|
+
</div>
|
|
40
|
+
<div class="form-row">
|
|
41
|
+
<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)
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="form-row" id="divSSML">
|
|
36
46
|
<label></label>
|
|
37
47
|
<input type="checkbox" id="node-input-ssml" style="margin-left: 0px; vertical-align: top; width: auto !important;"> <label style="width:auto !important;"> Enable SSML (unsupported by Google without authentication)</label>
|
|
38
48
|
</div>
|
|
@@ -129,8 +139,9 @@
|
|
|
129
139
|
playertype: { value: "sonos", required: false },
|
|
130
140
|
speakingrate: { value: "1", required: false },
|
|
131
141
|
speakingpitch: { value: "0", required: false },
|
|
132
|
-
unmuteIfMuted: { value: true }
|
|
133
|
-
|
|
142
|
+
unmuteIfMuted: { value: true },
|
|
143
|
+
elevenlabsStability: { value: "0.5", required: false },
|
|
144
|
+
elevenlabsSimilarity_boost: { value: "0.5", required: false },
|
|
134
145
|
},
|
|
135
146
|
inputs: 1,
|
|
136
147
|
outputs: 2,
|
|
@@ -163,8 +174,15 @@
|
|
|
163
174
|
getVoices();
|
|
164
175
|
if (oNodeServer.ttsservice === "googletts") {
|
|
165
176
|
$("#divGoogleTTSAudioConfig").show();
|
|
177
|
+
$("#divElevenLabsOptions").hide();
|
|
178
|
+
} else if (oNodeServer.ttsservice === "elevenlabs") {
|
|
179
|
+
$("#divGoogleTTSAudioConfig").hide();
|
|
180
|
+
$("#divElevenLabsOptions").show();
|
|
181
|
+
$("#divSSML").hide();
|
|
166
182
|
} else {
|
|
167
183
|
$("#divGoogleTTSAudioConfig").hide();
|
|
184
|
+
$("#divElevenLabsOptions").hide();
|
|
185
|
+
$("#divSSML").show();
|
|
168
186
|
}
|
|
169
187
|
} catch (error) {
|
|
170
188
|
}
|
|
@@ -564,108 +564,145 @@ module.exports = function (RED) {
|
|
|
564
564
|
|
|
565
565
|
|
|
566
566
|
while (node.tempMSGStorage.length > 0) {
|
|
567
|
-
node.currentMSGbeingSpoken = node.tempMSGStorage[0];// Advise the whole node of the currently spoken MSG
|
|
567
|
+
node.currentMSGbeingSpoken = node.tempMSGStorage.shift()//node.tempMSGStorage[0];// Advise the whole node of the currently spoken MSG
|
|
568
568
|
const msg = node.currentMSGbeingSpoken.payload.toString(); // Get the text to be spoken
|
|
569
|
-
node.tempMSGStorage.splice(0, 1); // Remove the first item in the array
|
|
570
|
-
|
|
569
|
+
//node.tempMSGStorage.splice(0, 1); // Remove the first item in the array
|
|
570
|
+
node.sFileToBePlayed = "";
|
|
571
571
|
node.setNodeStatus({ fill: "gray", shape: "ring", text: "Read " + msg });
|
|
572
572
|
|
|
573
573
|
// 04/12/2020 check what really is the file to be played
|
|
574
574
|
if (msg.toLowerCase().startsWith("http://") || msg.toLowerCase().startsWith("https://")) {
|
|
575
575
|
RED.log.info('ttsultimate: HTTP filename: ' + msg);
|
|
576
|
-
sFileToBePlayed = msg;
|
|
576
|
+
node.sFileToBePlayed = msg;
|
|
577
577
|
} else if (msg.indexOf("OwnFile_") !== -1) {
|
|
578
578
|
RED.log.info('ttsultimate: OwnFile .MP3, skip tts, filename: ' + msg);
|
|
579
|
-
sFileToBePlayed = path.join(node.userDir, "ttspermanentfiles", msg);
|
|
579
|
+
node.sFileToBePlayed = path.join(node.userDir, "ttspermanentfiles", msg);
|
|
580
580
|
} else if (msg.indexOf("Hailing_") !== -1) {
|
|
581
581
|
RED.log.info('ttsultimate: Hailing .MP3, skip tts, filename: ' + msg);
|
|
582
|
-
sFileToBePlayed = path.join(node.userDir, "hailingpermanentfiles", msg);
|
|
582
|
+
node.sFileToBePlayed = path.join(node.userDir, "hailingpermanentfiles", msg);
|
|
583
583
|
} else {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
node.
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
}
|
|
606
|
-
|
|
584
|
+
try {
|
|
585
|
+
// No file in cache. Download from tts service
|
|
586
|
+
var data = undefined;
|
|
587
|
+
if (node.server.ttsservice === "polly") {
|
|
588
|
+
var params = {
|
|
589
|
+
OutputFormat: "mp3",
|
|
590
|
+
SampleRate: '22050',
|
|
591
|
+
Text: msg,
|
|
592
|
+
TextType: node.ssml ? 'ssml' : 'text'
|
|
593
|
+
};
|
|
594
|
+
// 02/03/2022 check wether standard or neural engine is POLLY is selected
|
|
595
|
+
if (node.voiceId.includes("#engineType:")) {
|
|
596
|
+
params.VoiceId = node.voiceId.split("#engineType:")[0];
|
|
597
|
+
params.Engine = node.voiceId.split("#engineType:")[1];
|
|
598
|
+
} else {
|
|
599
|
+
params.VoiceId = node.voiceId;
|
|
600
|
+
}
|
|
601
|
+
// Download or read from cache
|
|
602
|
+
node.sFileToBePlayed = getFilename(msg, params);
|
|
603
|
+
node.sFileToBePlayed = path.join(node.userDir, "ttsfiles", node.sFileToBePlayed);
|
|
604
|
+
if (!fs.existsSync(node.sFileToBePlayed)) {
|
|
605
|
+
node.setNodeStatus({ fill: 'blue', shape: 'ring', text: 'Download using' + node.server.ttsservice });
|
|
607
606
|
data = await synthesizeSpeechPolly([node.server.polly, params]);
|
|
608
|
-
} else
|
|
609
|
-
node.setNodeStatus({ fill: 'green', shape: 'ring', text: '
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
}
|
|
617
|
-
|
|
607
|
+
} else {
|
|
608
|
+
node.setNodeStatus({ fill: 'green', shape: 'ring', text: 'Reading offline from cache' });
|
|
609
|
+
}
|
|
610
|
+
} else if (node.server.ttsservice === "googletts") {
|
|
611
|
+
// VoiceId is: name + "#" + languageCode + "#" + ssmlGender
|
|
612
|
+
// speakingRate tra 0.25 e 4.0
|
|
613
|
+
// pitch tra -20.0 e 20.0
|
|
614
|
+
const params = {
|
|
615
|
+
voice: { name: node.voiceId.split("#")[0], languageCode: node.voiceId.split("#")[1], ssmlGender: node.voiceId.split("#")[2] },
|
|
616
|
+
audioConfig: { audioEncoding: "MP3", speakingRate: parseFloat(node.speakingrate), pitch: parseFloat(node.speakingpitch), },
|
|
617
|
+
};
|
|
618
|
+
params.input = node.ssml === false ? { text: msg } : { ssml: msg };
|
|
619
|
+
|
|
620
|
+
// Download or read from cache
|
|
621
|
+
node.sFileToBePlayed = getFilename(msg, params);
|
|
622
|
+
node.sFileToBePlayed = path.join(node.userDir, "ttsfiles", node.sFileToBePlayed);
|
|
623
|
+
if (!fs.existsSync(node.sFileToBePlayed)) {
|
|
624
|
+
node.setNodeStatus({ fill: 'blue', shape: 'ring', text: 'Download using' + node.server.ttsservice });
|
|
618
625
|
data = await synthesizeSpeechGoogleTTS([node.server.googleTTS, params]);
|
|
619
|
-
} else
|
|
620
|
-
node.setNodeStatus({ fill: 'green', shape: 'ring', text: '
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
626
|
+
} else {
|
|
627
|
+
node.setNodeStatus({ fill: 'green', shape: 'ring', text: 'Reading offline from cache' });
|
|
628
|
+
}
|
|
629
|
+
} else if (node.server.ttsservice === "googletranslate") {
|
|
630
|
+
// VoiceId is: code. SSML is not supported by google translate
|
|
631
|
+
if (node.voiceId === "cmn-Hant-TW") node.voiceId = "zh-CN"; // 06/08/2022 fix for a wrong voiceid sent by google translate as voice code
|
|
632
|
+
const params = {
|
|
633
|
+
text: msg,
|
|
634
|
+
voice: node.voiceId,
|
|
635
|
+
slow: false // optional
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// Download or read from cache
|
|
639
|
+
node.sFileToBePlayed = getFilename(msg, params);
|
|
640
|
+
node.sFileToBePlayed = path.join(node.userDir, "ttsfiles", node.sFileToBePlayed);
|
|
641
|
+
if (!fs.existsSync(node.sFileToBePlayed)) {
|
|
642
|
+
node.setNodeStatus({ fill: 'blue', shape: 'ring', text: 'Download using' + node.server.ttsservice });
|
|
628
643
|
data = await synthesizeSpeechGoogleTranslate(node.server.googleTranslateTTS, params);
|
|
629
|
-
} else
|
|
630
|
-
node.setNodeStatus({ fill: 'green', shape: 'ring', text: '
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
644
|
+
} else {
|
|
645
|
+
node.setNodeStatus({ fill: 'green', shape: 'ring', text: 'Reading offline from cache' });
|
|
646
|
+
}
|
|
647
|
+
} else if (node.server.ttsservice === "microsoftazuretts") {
|
|
648
|
+
// VoiceId is: code
|
|
649
|
+
const params = {
|
|
650
|
+
text: msg,
|
|
651
|
+
voice: node.voiceId
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
// Download or read from cache
|
|
655
|
+
node.sFileToBePlayed = getFilename(msg, params);
|
|
656
|
+
node.sFileToBePlayed = path.join(node.userDir, "ttsfiles", node.sFileToBePlayed);
|
|
657
|
+
if (!fs.existsSync(node.sFileToBePlayed)) {
|
|
658
|
+
node.setNodeStatus({ fill: 'blue', shape: 'ring', text: 'Download using' + node.server.ttsservice });
|
|
636
659
|
data = await synthesizeSpeechMicrosoftAzureTTS(node.server.microsoftAzureTTS, params);
|
|
637
|
-
} else
|
|
638
|
-
node.setNodeStatus({ fill: 'green', shape: 'ring', text: '
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
660
|
+
} else {
|
|
661
|
+
node.setNodeStatus({ fill: 'green', shape: 'ring', text: 'Reading offline from cache' });
|
|
662
|
+
}
|
|
663
|
+
} else if (node.server.ttsservice === "elevenlabs") {
|
|
664
|
+
// VoiceId is: code
|
|
665
|
+
const params = {
|
|
666
|
+
text: msg,
|
|
667
|
+
voice: node.voiceId,
|
|
668
|
+
model_id: "eleven_monolingual_v1",
|
|
669
|
+
voice_settings: {
|
|
670
|
+
stability: config.elevenlabsStability,
|
|
671
|
+
similarity_boost: config.elevenlabsSimilarity_boost
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
// Download or read from cache
|
|
675
|
+
node.sFileToBePlayed = getFilename(msg, params);
|
|
676
|
+
node.sFileToBePlayed = path.join(node.userDir, "ttsfiles", node.sFileToBePlayed);
|
|
677
|
+
if (!fs.existsSync(node.sFileToBePlayed)) {
|
|
678
|
+
node.setNodeStatus({ fill: 'blue', shape: 'ring', text: 'Download using' + node.server.ttsservice });
|
|
644
679
|
data = await synthesizeSpeechElevenLabs(node.server.elevenlabsTTS, params);
|
|
680
|
+
} else {
|
|
681
|
+
node.setNodeStatus({ fill: 'green', shape: 'ring', text: 'Reading offline from cache' });
|
|
645
682
|
}
|
|
683
|
+
}
|
|
646
684
|
|
|
647
|
-
|
|
685
|
+
// Save the downloaded file into the cache
|
|
686
|
+
if (data !== undefined) {
|
|
648
687
|
try {
|
|
649
|
-
|
|
688
|
+
console.log("Salvelox " + node.sFileToBePlayed)
|
|
689
|
+
fs.writeFileSync(node.sFileToBePlayed, data);
|
|
650
690
|
} catch (error) {
|
|
651
691
|
RED.log.error("ttsultimate: node id: " + node.id + " Unable to save the file " + error.message);
|
|
652
|
-
node.setNodeStatus({ fill: "red", shape: "ring", text: "Unable to save the file " + sFileToBePlayed + " " + error.message });
|
|
692
|
+
node.setNodeStatus({ fill: "red", shape: "ring", text: "Unable to save the file " + node.sFileToBePlayed + " " + error.message });
|
|
653
693
|
throw (error);
|
|
654
694
|
}
|
|
655
|
-
|
|
656
|
-
} catch (error) {
|
|
657
|
-
RED.log.error("ttsultimate: node id: " + node.id + " Error Downloading TTS: " + error.message + ". THE TTS SERVICE MAY BE DOWN.");
|
|
658
|
-
node.setNodeStatus({ fill: 'red', shape: 'ring', text: 'Error Downloading TTS:' + error.message });
|
|
659
|
-
sFileToBePlayed = "";
|
|
660
695
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
696
|
+
|
|
697
|
+
} catch (error) {
|
|
698
|
+
RED.log.error("ttsultimate: node id: " + node.id + " Error Downloading TTS: " + error.message + ". THE TTS SERVICE MAY BE DOWN.");
|
|
699
|
+
node.setNodeStatus({ fill: 'red', shape: 'ring', text: 'Error Downloading TTS:' + error.message });
|
|
700
|
+
node.sFileToBePlayed = "";
|
|
664
701
|
}
|
|
665
702
|
}
|
|
666
703
|
|
|
667
704
|
// Ready to play
|
|
668
|
-
if (sFileToBePlayed !== "") {
|
|
705
|
+
if (node.sFileToBePlayed !== "") {
|
|
669
706
|
|
|
670
707
|
//#region Now i am ready to play the file
|
|
671
708
|
if (node.playertype === "sonos") {
|
|
@@ -674,8 +711,8 @@ module.exports = function (RED) {
|
|
|
674
711
|
node.setNodeStatus({ fill: 'green', shape: 'ring', text: 'Play ' + msg });
|
|
675
712
|
|
|
676
713
|
// Play directly files starting with http://
|
|
677
|
-
if (!sFileToBePlayed.toLowerCase().startsWith("http://") && !sFileToBePlayed.toLowerCase().startsWith("https://")) {
|
|
678
|
-
sFileToBePlayed = node.sNoderedURL + "/tts/tts.mp3?f=" + encodeURIComponent(sFileToBePlayed);
|
|
714
|
+
if (!node.sFileToBePlayed.toLowerCase().startsWith("http://") && !node.sFileToBePlayed.toLowerCase().startsWith("https://")) {
|
|
715
|
+
node.sFileToBePlayed = node.sNoderedURL + "/tts/tts.mp3?f=" + encodeURIComponent(node.sFileToBePlayed);
|
|
679
716
|
}
|
|
680
717
|
|
|
681
718
|
// Set Volume
|
|
@@ -708,11 +745,11 @@ module.exports = function (RED) {
|
|
|
708
745
|
};
|
|
709
746
|
|
|
710
747
|
} catch (error) {
|
|
711
|
-
RED.log.error("ttsultimate: Unable to set the volume for " + sFileToBePlayed);
|
|
748
|
+
RED.log.error("ttsultimate: Unable to set the volume for " + node.sFileToBePlayed);
|
|
712
749
|
}
|
|
713
750
|
try {
|
|
714
751
|
|
|
715
|
-
await setAVTransportURISync(sFileToBePlayed);
|
|
752
|
+
await setAVTransportURISync(node.sFileToBePlayed);
|
|
716
753
|
|
|
717
754
|
// Wait for start playing
|
|
718
755
|
var state = "";
|
|
@@ -769,14 +806,14 @@ module.exports = function (RED) {
|
|
|
769
806
|
|
|
770
807
|
} catch (error) {
|
|
771
808
|
if (node.timerbTimeOutPlay !== null) clearTimeout(node.timerbTimeOutPlay); // Clear the player timeout
|
|
772
|
-
RED.log.error("ttsultimate: Error HandleQueue for " + sFileToBePlayed + " " + error.message);
|
|
809
|
+
RED.log.error("ttsultimate: Error HandleQueue for " + node.sFileToBePlayed + " " + error.message);
|
|
773
810
|
node.setNodeStatus({ fill: 'red', shape: 'dot', text: 'Error ' + msg + " " + error.message });
|
|
774
811
|
}
|
|
775
812
|
|
|
776
813
|
} else if (node.playertype === "noplayer") {
|
|
777
814
|
// Output only the filename
|
|
778
815
|
if (noPlayerFileArray === undefined || noPlayerFileArray === null) var noPlayerFileArray = [];
|
|
779
|
-
noPlayerFileArray.push({ file: sFileToBePlayed });
|
|
816
|
+
noPlayerFileArray.push({ file: node.sFileToBePlayed });
|
|
780
817
|
}
|
|
781
818
|
}
|
|
782
819
|
//#endregion
|
|
@@ -1163,7 +1200,7 @@ module.exports = function (RED) {
|
|
|
1163
1200
|
}
|
|
1164
1201
|
return new Promise((resolve, reject) => {
|
|
1165
1202
|
// "model_id": "eleven_multilingual_v1",
|
|
1166
|
-
ttsService.textToSpeechStream(node.server.credentials.elevenlabsKey, params.voice, params.text, null,null,"eleven_multilingual_v1").then((res) => {
|
|
1203
|
+
ttsService.textToSpeechStream(node.server.credentials.elevenlabsKey, params.voice, params.text, null, null, "eleven_multilingual_v1").then((res) => {
|
|
1167
1204
|
try {
|
|
1168
1205
|
if (res !== undefined) {
|
|
1169
1206
|
resolve(stream2buffer(res));
|
|
@@ -1178,11 +1215,11 @@ module.exports = function (RED) {
|
|
|
1178
1215
|
}
|
|
1179
1216
|
|
|
1180
1217
|
// 04/01/2021 hashing filename to avoid issues with long filenames.
|
|
1181
|
-
function getFilename(_text,
|
|
1182
|
-
let sTextToBeHashed = _text.concat(
|
|
1218
|
+
function getFilename(_text, _params) {
|
|
1219
|
+
let sTextToBeHashed = _text.concat(JSON.stringify(_params));
|
|
1183
1220
|
const hashSum = crypto.createHash('md5');
|
|
1184
1221
|
hashSum.update(sTextToBeHashed);
|
|
1185
|
-
return hashSum.digest('hex') + "."
|
|
1222
|
+
return hashSum.digest('hex') + ".mp3";
|
|
1186
1223
|
}
|
|
1187
1224
|
|
|
1188
1225
|
function notifyError(msg, err) {
|