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 CHANGED
@@ -2,6 +2,15 @@
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
+ <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.1",
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 class="form-row">
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
- var sFileToBePlayed = "";
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
- sFileToBePlayed = getFilename(msg, node.voiceId, node.ssml, "mp3", node.speakingpitch, node.speakingrate);
585
- sFileToBePlayed = path.join(node.userDir, "ttsfiles", sFileToBePlayed);
586
- // Check if cached
587
- if (!fs.existsSync(sFileToBePlayed)) {
588
- try {
589
- // No file in cache. Download from tts service
590
- var data;
591
- if (node.server.ttsservice === "polly") {
592
- node.setNodeStatus({ fill: 'green', shape: 'ring', text: 'Downloading from Amazon...' });
593
- var params = {
594
- OutputFormat: "mp3",
595
- SampleRate: '22050',
596
- Text: msg,
597
- TextType: node.ssml ? 'ssml' : 'text'
598
- };
599
- // 02/03/2022 check wether standard or neural engine is POLLY is selected
600
- if (node.voiceId.includes("#engineType:")) {
601
- params.VoiceId = node.voiceId.split("#engineType:")[0];
602
- params.Engine = node.voiceId.split("#engineType:")[1];
603
- } else {
604
- params.VoiceId = node.voiceId;
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 if (node.server.ttsservice === "googletts") {
609
- node.setNodeStatus({ fill: 'green', shape: 'ring', text: 'Downloading from Google TTS...' });
610
- // VoiceId is: name + "#" + languageCode + "#" + ssmlGender
611
- // speakingRate tra 0.25 e 4.0
612
- // pitch tra -20.0 e 20.0
613
- const params = {
614
- voice: { name: node.voiceId.split("#")[0], languageCode: node.voiceId.split("#")[1], ssmlGender: node.voiceId.split("#")[2] },
615
- audioConfig: { audioEncoding: "MP3", speakingRate: parseFloat(node.speakingrate), pitch: parseFloat(node.speakingpitch), },
616
- };
617
- params.input = node.ssml === false ? { text: msg } : { ssml: msg };
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 if (node.server.ttsservice === "googletranslate") {
620
- node.setNodeStatus({ fill: 'green', shape: 'ring', text: 'Downloading from Google Translate...' });
621
- // VoiceId is: code. SSML is not supported by google translate
622
- 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
623
- const params = {
624
- text: msg,
625
- voice: node.voiceId,
626
- slow: false // optional
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 if (node.server.ttsservice === "microsoftazuretts") {
630
- node.setNodeStatus({ fill: 'green', shape: 'ring', text: 'Downloading from Microsoft Azure TTS...' });
631
- // VoiceId is: code
632
- const params = {
633
- text: msg,
634
- voice: node.voiceId
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 if (node.server.ttsservice === "elevenlabs") {
638
- node.setNodeStatus({ fill: 'green', shape: 'ring', text: 'Downloading from elevenLabs TTS...' });
639
- // VoiceId is: code
640
- const params = {
641
- text: msg,
642
- voice: node.voiceId
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
- // Save the downloaded file into the cache
685
+ // Save the downloaded file into the cache
686
+ if (data !== undefined) {
648
687
  try {
649
- fs.writeFileSync(sFileToBePlayed, data);
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
- else {
663
- node.setNodeStatus({ fill: 'green', shape: 'ring', text: 'Reading offline from cache' });
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, _sVoice, _isSSML, _extension, _speakingpitch, _speakingrate) {
1182
- let sTextToBeHashed = _text.concat(_sVoice, _isSSML, _speakingpitch, _speakingrate);
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') + "." + _extension;
1222
+ return hashSum.digest('hex') + ".mp3";
1186
1223
  }
1187
1224
 
1188
1225
  function notifyError(msg, err) {