node-red-contrib-tts-ultimate 3.0.7 → 3.2.0
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 +31 -0
- package/README.md +8 -6
- package/package.json +8 -8
- package/scripts/discover-dlna.js +44 -0
- package/scripts/verify-googletranslate-split.js +1 -1
- package/ttsultimate/lib/dlna-discovery.js +98 -0
- package/ttsultimate/lib/dlna-player.js +163 -0
- package/ttsultimate/lib/googlecast-discovery.js +69 -0
- package/ttsultimate/lib/googletranslate.js +140 -0
- package/ttsultimate/ttsultimate-config copy.js +1 -1
- package/ttsultimate/ttsultimate-config.html +3 -3
- package/ttsultimate/ttsultimate-config.js +138 -28
- package/ttsultimate/ttsultimate.html +260 -28
- package/ttsultimate/ttsultimate.js +155 -11
|
@@ -218,8 +218,8 @@
|
|
|
218
218
|
# TTS Service node
|
|
219
219
|
Here you can set all parameters you need. All nodes will refer to this config node, so you need to set it only once.<br/>
|
|
220
220
|
IF YOU RUN NODE-RED BEHIND DOCKER OR SOMETHING ELSE, BE AWARE: <br/>
|
|
221
|
-
PORT USED BY THE NODE ARE 1980 (DEFAULT) AND 1400 (FOR SONOS DISCOVER). <br/>
|
|
222
|
-
PLEASE ALLOW MDNS AND UDP AS WELL
|
|
221
|
+
PORT USED BY THE NODE ARE 1980 (DEFAULT, HTTP FILE SERVER) AND 1400 (FOR SONOS DISCOVER). <br/>
|
|
222
|
+
PLEASE ALLOW MDNS AND UDP AS WELL (USED TO DISCOVER SONOS, GOOGLE CAST AND DLNA/UPNP DEVICES)
|
|
223
223
|
|
|
224
224
|
**TTS Service**<br/>
|
|
225
225
|
You can choose between Google (without credentials), Google TTS (require credentials and registration to google), ElevenLabs or Voice.ai TTS engines.<br/>
|
|
@@ -262,7 +262,7 @@ PLEASE ALLOW MDNS AND UDP AS WELL
|
|
|
262
262
|
set IP of your node-red machine. Write **AUTODISCOVER** to allow the node to auto discover your IP.
|
|
263
263
|
|
|
264
264
|
**Host Port**<br/>
|
|
265
|
-
Sonos will connect to this port
|
|
265
|
+
The players (Sonos, Google Cast, DLNA/UPnP renderers) will connect to this port to fetch the TTS audio. Default 1980. Choose a free port. Do not use 1880 or any other port already in use on your computer. The port must be reachable from the players on your network.
|
|
266
266
|
|
|
267
267
|
**TTS Cache**
|
|
268
268
|
<br/>
|
|
@@ -11,7 +11,7 @@ module.exports = function (RED) {
|
|
|
11
11
|
|
|
12
12
|
// Setting up the engines
|
|
13
13
|
const GoogleTTS = require('@google-cloud/text-to-speech');
|
|
14
|
-
const GoogleTranslate = require('
|
|
14
|
+
const GoogleTranslate = require('./lib/googletranslate'); // Native TTS without credentials, limited to 200 chars per row.
|
|
15
15
|
const elevenlabsTTS = require("elevenlabs-node"); // 03/08/2023
|
|
16
16
|
const ElevenLabsClient = require("elevenlabs").ElevenLabsClient;
|
|
17
17
|
|
|
@@ -20,6 +20,8 @@ module.exports = function (RED) {
|
|
|
20
20
|
var formidable = require('formidable');
|
|
21
21
|
const oOS = require('os');
|
|
22
22
|
const sonos = require('sonos');
|
|
23
|
+
const dlnaDiscovery = require('./lib/dlna-discovery'); // 06/2026 SSDP discovery of DLNA/UPnP renderers
|
|
24
|
+
const castDiscovery = require('./lib/googlecast-discovery'); // 06/2026 mDNS discovery of Google Cast devices
|
|
23
25
|
|
|
24
26
|
const VOICEAI_API_BASE_URL = "https://dev.voice.ai/api/v1/tts";
|
|
25
27
|
|
|
@@ -326,6 +328,38 @@ module.exports = function (RED) {
|
|
|
326
328
|
});
|
|
327
329
|
|
|
328
330
|
|
|
331
|
+
// 06/2026 Discover DLNA/UPnP MediaRenderers on the network (for the "dlna" player type)
|
|
332
|
+
RED.httpAdmin.get("/ttsultimateDiscoverDLNA", RED.auth.needsPermission('TTSConfigNode.read'), function (req, res) {
|
|
333
|
+
dlnaDiscovery.discoverRenderers({ timeoutMs: 4000 }).then((devices) => {
|
|
334
|
+
// Same shape as sonosgetAllGroups: { name, host }. For DLNA the "host" is the description URL.
|
|
335
|
+
const list = devices.map((d) => ({
|
|
336
|
+
name: d.name || "Renderer",
|
|
337
|
+
host: d.location
|
|
338
|
+
}));
|
|
339
|
+
res.json(list);
|
|
340
|
+
}).catch((error) => {
|
|
341
|
+
RED.log.warn('ttsultimate-config ' + node.id + ': Error discovering DLNA renderers ' + error.message);
|
|
342
|
+
res.json("ERRORDISCOVERY");
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
// 06/2026 Discover Google Cast devices (Chromecast / Nest) on the network (for the "googlecast" player type)
|
|
348
|
+
RED.httpAdmin.get("/ttsultimateDiscoverCast", RED.auth.needsPermission('TTSConfigNode.read'), function (req, res) {
|
|
349
|
+
castDiscovery.discoverCastDevices({ timeoutMs: 4000 }).then((devices) => {
|
|
350
|
+
// Same shape as sonosgetAllGroups: { name, host }
|
|
351
|
+
const list = devices.map((d) => ({
|
|
352
|
+
name: d.name,
|
|
353
|
+
host: d.host
|
|
354
|
+
}));
|
|
355
|
+
res.json(list);
|
|
356
|
+
}).catch((error) => {
|
|
357
|
+
RED.log.warn('ttsultimate-config ' + node.id + ': Error discovering Google Cast devices ' + error.message);
|
|
358
|
+
res.json("ERRORDISCOVERY");
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
|
|
329
363
|
// 09/03/2020 Get list of filenames in hailing folder
|
|
330
364
|
RED.httpAdmin.get("/getHailingFilesList", RED.auth.needsPermission('TTSConfigNode.read'), function (req, res) {
|
|
331
365
|
var jListOwnFiles = [];
|
|
@@ -505,6 +539,46 @@ module.exports = function (RED) {
|
|
|
505
539
|
|
|
506
540
|
});
|
|
507
541
|
|
|
542
|
+
// 10/06/2026 Supergiovane, get the available ElevenLabs models with their capabilities.
|
|
543
|
+
// Used to dynamically populate the Model dropdown and enable/disable model-specific options.
|
|
544
|
+
RED.httpAdmin.get("/ttsgetmodels" + encodeURIComponent(node.id), RED.auth.needsPermission('TTSConfigNode.read'), function (req, res) {
|
|
545
|
+
const ttsservice = req.query.ttsservice || node.ttsservice;
|
|
546
|
+
if (!ttsservice || !ttsservice.includes("elevenlabs")) {
|
|
547
|
+
return res.json([]);
|
|
548
|
+
}
|
|
549
|
+
const apiKey = node.credentials.elevenlabsKey;
|
|
550
|
+
if (!apiKey || apiKey.trim() === "") {
|
|
551
|
+
return res.json([{ model_id: "", name: "ElevenLabs API key missing. Please configure, deploy and restart node-red.", error: true }]);
|
|
552
|
+
}
|
|
553
|
+
(async () => {
|
|
554
|
+
try {
|
|
555
|
+
const r = await fetch("https://api.elevenlabs.io/v1/models", {
|
|
556
|
+
method: "GET",
|
|
557
|
+
headers: { "xi-api-key": apiKey }
|
|
558
|
+
});
|
|
559
|
+
if (!r.ok) {
|
|
560
|
+
const body = await r.text().catch(() => "");
|
|
561
|
+
throw new Error(`HTTP ${r.status} ${r.statusText}${body ? " - " + body : ""}`);
|
|
562
|
+
}
|
|
563
|
+
const models = await r.json();
|
|
564
|
+
const list = (Array.isArray(models) ? models : [])
|
|
565
|
+
.filter(m => m && m.model_id && m.can_do_text_to_speech !== false)
|
|
566
|
+
.map(m => ({
|
|
567
|
+
model_id: m.model_id,
|
|
568
|
+
name: m.name || m.model_id,
|
|
569
|
+
can_use_style: m.can_use_style === true,
|
|
570
|
+
can_use_speaker_boost: m.can_use_speaker_boost === true,
|
|
571
|
+
maximum_text_length_per_request: m.maximum_text_length_per_request,
|
|
572
|
+
languages: Array.isArray(m.languages) ? m.languages.map(l => l.name || l.language_id).filter(Boolean) : []
|
|
573
|
+
}));
|
|
574
|
+
res.json(list);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
RED.log.error('ttsultimate-config ' + node.id + ': Error getting ElevenLabs models: ' + error.message);
|
|
577
|
+
res.json([{ model_id: "", name: "Error getting ElevenLabs models: " + error.message + " Check credentials, deploy and restart node-red.", error: true }]);
|
|
578
|
+
}
|
|
579
|
+
})();
|
|
580
|
+
});
|
|
581
|
+
|
|
508
582
|
// ########################################################
|
|
509
583
|
//#endregion
|
|
510
584
|
|
|
@@ -640,45 +714,81 @@ module.exports = function (RED) {
|
|
|
640
714
|
var url_parts = url.parse(req.url, true);
|
|
641
715
|
var query = url_parts.query;
|
|
642
716
|
|
|
643
|
-
res.setHeader('Content-Disposition', 'attachment; filename=tts.mp3')
|
|
644
717
|
if (!query || query.f === undefined || query.f === null) {
|
|
645
|
-
res.
|
|
646
|
-
res.end();
|
|
718
|
+
res.statusCode = 400;
|
|
719
|
+
res.end("File not specified");
|
|
647
720
|
return;
|
|
648
721
|
}
|
|
649
722
|
|
|
650
723
|
const requestedPath = query.f.toString();
|
|
651
|
-
if (fs.existsSync(requestedPath)) {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
if (path.extname(resolvedRequested) === ".mp3" && isInsideRoot) {
|
|
659
|
-
var readStream = fs.createReadStream(resolvedRequested);
|
|
660
|
-
readStream.on("error", function (error) {
|
|
661
|
-
RED.log.error("ttsultimate-config " + node.id + ": Playsonos error opening stream : " + resolvedRequested + ' : ' + error);
|
|
662
|
-
res.end();
|
|
663
|
-
return;
|
|
664
|
-
});
|
|
665
|
-
readStream.pipe(res);
|
|
666
|
-
} else {
|
|
667
|
-
res.write("NOT ALLOWED");
|
|
668
|
-
res.end();
|
|
669
|
-
}
|
|
724
|
+
if (!fs.existsSync(requestedPath)) {
|
|
725
|
+
RED.log.error("ttsultimate-config " + node.id + ": Playsonos RED.httpAdmin file not found: " + query.f);
|
|
726
|
+
res.statusCode = 404;
|
|
727
|
+
res.end("File not found");
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
670
730
|
|
|
671
|
-
|
|
731
|
+
// Security check: allow only mp3 files under the configured storage folder.
|
|
732
|
+
// http://localhost:1980/tts?f=/etc/passwd
|
|
733
|
+
const resolvedRequested = path.resolve(requestedPath);
|
|
734
|
+
const resolvedAllowedRoot = path.resolve(node.TTSRootFolderPath);
|
|
735
|
+
const isInsideRoot =
|
|
736
|
+
resolvedRequested === resolvedAllowedRoot || resolvedRequested.startsWith(resolvedAllowedRoot + path.sep);
|
|
737
|
+
if (path.extname(resolvedRequested) !== ".mp3" || !isInsideRoot) {
|
|
738
|
+
res.statusCode = 403;
|
|
739
|
+
res.end("NOT ALLOWED");
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
672
742
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
743
|
+
// Serve as a proper streaming media response. Strict DLNA renderers (e.g. some TVs)
|
|
744
|
+
// require Content-Type, Content-Length, byte-range support and DLNA headers to start playing.
|
|
745
|
+
const total = fs.statSync(resolvedRequested).size;
|
|
746
|
+
res.setHeader('Content-Type', 'audio/mpeg');
|
|
747
|
+
res.setHeader('Content-Disposition', 'inline; filename="tts.mp3"');
|
|
748
|
+
res.setHeader('Accept-Ranges', 'bytes');
|
|
749
|
+
res.setHeader('transferMode.dlna.org', 'Streaming');
|
|
750
|
+
res.setHeader('contentFeatures.dlna.org', 'DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000');
|
|
751
|
+
|
|
752
|
+
// HEAD: headers only (many DLNA renderers probe with HEAD before playing).
|
|
753
|
+
if (req.method === 'HEAD') {
|
|
754
|
+
res.setHeader('Content-Length', total);
|
|
755
|
+
res.statusCode = 200;
|
|
676
756
|
res.end();
|
|
757
|
+
return;
|
|
677
758
|
}
|
|
678
759
|
|
|
760
|
+
let readStream;
|
|
761
|
+
const range = req.headers.range;
|
|
762
|
+
if (range) {
|
|
763
|
+
const m = /bytes=(\d*)-(\d*)/.exec(range);
|
|
764
|
+
let start = m && m[1] ? parseInt(m[1], 10) : 0;
|
|
765
|
+
let end = m && m[2] ? parseInt(m[2], 10) : total - 1;
|
|
766
|
+
if (isNaN(start)) start = 0;
|
|
767
|
+
if (isNaN(end) || end >= total) end = total - 1;
|
|
768
|
+
if (start > end || start >= total) {
|
|
769
|
+
res.statusCode = 416;
|
|
770
|
+
res.setHeader('Content-Range', 'bytes */' + total);
|
|
771
|
+
res.end();
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
res.statusCode = 206;
|
|
775
|
+
res.setHeader('Content-Range', 'bytes ' + start + '-' + end + '/' + total);
|
|
776
|
+
res.setHeader('Content-Length', (end - start) + 1);
|
|
777
|
+
readStream = fs.createReadStream(resolvedRequested, { start: start, end: end });
|
|
778
|
+
} else {
|
|
779
|
+
res.statusCode = 200;
|
|
780
|
+
res.setHeader('Content-Length', total);
|
|
781
|
+
readStream = fs.createReadStream(resolvedRequested);
|
|
782
|
+
}
|
|
783
|
+
readStream.on("error", function (error) {
|
|
784
|
+
RED.log.error("ttsultimate-config " + node.id + ": Playsonos error opening stream : " + resolvedRequested + ' : ' + error);
|
|
785
|
+
try { res.end(); } catch (e) { }
|
|
786
|
+
});
|
|
787
|
+
readStream.pipe(res);
|
|
788
|
+
|
|
679
789
|
} catch (error) {
|
|
680
790
|
RED.log.error("ttsultimate-config " + node.id + ": Playsonos RED.httpAdmin error: " + error);
|
|
681
|
-
res.end();
|
|
791
|
+
try { res.end(); } catch (e) { }
|
|
682
792
|
}
|
|
683
793
|
|
|
684
794
|
}
|