n8n-nodes-tts-bigboss 1.0.3 → 1.0.6
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/dist/TTSBigBoss.node.js +133 -30
- package/nodes/TTSBigBoss/TTSBigBoss.node.ts +151 -30
- package/package.json +1 -1
package/dist/TTSBigBoss.node.js
CHANGED
|
@@ -44,32 +44,33 @@ const os = __importStar(require("os"));
|
|
|
44
44
|
const child_process = __importStar(require("child_process"));
|
|
45
45
|
const ws_1 = __importDefault(require("ws"));
|
|
46
46
|
const https = __importStar(require("https"));
|
|
47
|
+
const http = __importStar(require("http"));
|
|
47
48
|
const stream = __importStar(require("stream"));
|
|
48
49
|
const util_1 = require("util");
|
|
49
50
|
const pipeline = (0, util_1.promisify)(stream.pipeline);
|
|
50
51
|
const PIPER_MODELS = [
|
|
51
|
-
{ name: 'Arabic (
|
|
52
|
-
{ name: 'Arabic (
|
|
53
|
-
{ name: 'English (US) - Lessac (Low
|
|
54
|
-
{ name: 'English (US) - Lessac (Medium
|
|
55
|
-
{ name: 'English (US) - Lessac (High
|
|
56
|
-
{ name: 'English (US) - Ryan (Low
|
|
57
|
-
{ name: 'English (US) - Ryan (Medium
|
|
58
|
-
{ name: 'English (US) - Ryan (High
|
|
59
|
-
{ name: 'English (US) - Amy (Low
|
|
60
|
-
{ name: 'English (US) - Amy (Medium
|
|
61
|
-
{ name: 'English (US) - Kathleen (Low
|
|
62
|
-
{ name: 'English (UK) - Alan (Low
|
|
63
|
-
{ name: 'English (UK) - Alan (Medium
|
|
64
|
-
{ name: 'English (UK) - Southern
|
|
65
|
-
{ name: 'French (
|
|
66
|
-
{ name: 'French (
|
|
67
|
-
{ name: 'Spanish (
|
|
68
|
-
{ name: 'Spanish (
|
|
69
|
-
{ name: 'German (
|
|
70
|
-
{ name: 'German (
|
|
71
|
-
{ name: 'German (
|
|
72
|
-
{ name: 'German (
|
|
52
|
+
{ name: 'Arabic (Jordan) - Kareem (Male) - Low', value: 'ar_JO-kareem-low' },
|
|
53
|
+
{ name: 'Arabic (Jordan) - Kareem (Male) - Medium', value: 'ar_JO-kareem-medium' },
|
|
54
|
+
{ name: 'English (US) - Lessac (Female) - Low', value: 'en_US-lessac-low' },
|
|
55
|
+
{ name: 'English (US) - Lessac (Female) - Medium', value: 'en_US-lessac-medium' },
|
|
56
|
+
{ name: 'English (US) - Lessac (Female) - High', value: 'en_US-lessac-high' },
|
|
57
|
+
{ name: 'English (US) - Ryan (Male) - Low', value: 'en_US-ryan-low' },
|
|
58
|
+
{ name: 'English (US) - Ryan (Male) - Medium', value: 'en_US-ryan-medium' },
|
|
59
|
+
{ name: 'English (US) - Ryan (Male) - High', value: 'en_US-ryan-high' },
|
|
60
|
+
{ name: 'English (US) - Amy (Female) - Low', value: 'en_US-amy-low' },
|
|
61
|
+
{ name: 'English (US) - Amy (Female) - Medium', value: 'en_US-amy-medium' },
|
|
62
|
+
{ name: 'English (US) - Kathleen (Female) - Low', value: 'en_US-kathleen-low' },
|
|
63
|
+
{ name: 'English (UK) - Alan (Male) - Low', value: 'en_GB-alan-low' },
|
|
64
|
+
{ name: 'English (UK) - Alan (Male) - Medium', value: 'en_GB-alan-medium' },
|
|
65
|
+
{ name: 'English (UK) - Southern Female - Low', value: 'en_GB-southern_english_female-low' },
|
|
66
|
+
{ name: 'French - Siwis (Female) - Low', value: 'fr_FR-siwis-low' },
|
|
67
|
+
{ name: 'French - Siwis (Female) - Medium', value: 'fr_FR-siwis-medium' },
|
|
68
|
+
{ name: 'Spanish (Spain) - Sharvard (Male) - Medium', value: 'es_ES-sharvard-medium' },
|
|
69
|
+
{ name: 'Spanish (Mexico) - Aldone (Male) - Medium', value: 'es_MX-aldona-medium' },
|
|
70
|
+
{ name: 'German - Eva (Female) - High', value: 'de_DE-eva_k-x_low' },
|
|
71
|
+
{ name: 'German - Thorsten (Male) - High', value: 'de_DE-thorsten-high' },
|
|
72
|
+
{ name: 'German - Thorsten (Male) - Medium', value: 'de_DE-thorsten-medium' },
|
|
73
|
+
{ name: 'German - Thorsten (Male) - Low', value: 'de_DE-thorsten-low' },
|
|
73
74
|
];
|
|
74
75
|
const EDGE_URL = 'wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1?TrustedClientToken=6A5AA1D4EAFF4E9FB37E23D68491D6F4';
|
|
75
76
|
const EDGE_VOICES = [
|
|
@@ -121,6 +122,11 @@ class TTSBigBoss {
|
|
|
121
122
|
value: 'piper_local',
|
|
122
123
|
description: 'Downloads and runs Piper locally (Offline). Good quality, fast.',
|
|
123
124
|
},
|
|
125
|
+
{
|
|
126
|
+
name: 'Coqui TTS (Local Server)',
|
|
127
|
+
value: 'coqui',
|
|
128
|
+
description: 'Connect to a running Coqui TTS/XTTS server (e.g. python3 coqui_server.py).',
|
|
129
|
+
},
|
|
124
130
|
{
|
|
125
131
|
name: 'System Command (Custom)',
|
|
126
132
|
value: 'system',
|
|
@@ -254,7 +260,7 @@ class TTSBigBoss {
|
|
|
254
260
|
engine: ['piper_local'],
|
|
255
261
|
},
|
|
256
262
|
},
|
|
257
|
-
description: 'Select a voice model. It will be downloaded automatically
|
|
263
|
+
description: 'Select a voice model. It will be downloaded automatically. Note: Official Piper Arabic only has "Kareem" (Male). For Female Arabic, please use the "Edge TTS" engine.',
|
|
258
264
|
},
|
|
259
265
|
{
|
|
260
266
|
displayName: 'Custom Model Name/URL',
|
|
@@ -270,6 +276,42 @@ class TTSBigBoss {
|
|
|
270
276
|
},
|
|
271
277
|
description: 'Name from Hugging Face (e.g. en_US-bryce-medium) or full URL to .onnx file.',
|
|
272
278
|
},
|
|
279
|
+
{
|
|
280
|
+
displayName: 'Server URL',
|
|
281
|
+
name: 'coquiUrl',
|
|
282
|
+
type: 'string',
|
|
283
|
+
default: 'http://localhost:5002/api/tts',
|
|
284
|
+
description: 'URL of the running Coqui server endpoint (usually /api/tts or /tts_stream).',
|
|
285
|
+
displayOptions: {
|
|
286
|
+
show: {
|
|
287
|
+
engine: ['coqui'],
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
displayName: 'Speaker ID / Wav Path',
|
|
293
|
+
name: 'coquiSpeaker',
|
|
294
|
+
type: 'string',
|
|
295
|
+
default: '',
|
|
296
|
+
description: 'Speaker ID (if multi-speaker) or path to reference wav (for cloning).',
|
|
297
|
+
displayOptions: {
|
|
298
|
+
show: {
|
|
299
|
+
engine: ['coqui'],
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
displayName: 'Language',
|
|
305
|
+
name: 'coquiLang',
|
|
306
|
+
type: 'string',
|
|
307
|
+
default: 'en',
|
|
308
|
+
description: 'Language code (e.g. en, ar, fr).',
|
|
309
|
+
displayOptions: {
|
|
310
|
+
show: {
|
|
311
|
+
engine: ['coqui'],
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
},
|
|
273
315
|
],
|
|
274
316
|
};
|
|
275
317
|
}
|
|
@@ -341,6 +383,41 @@ class TTSBigBoss {
|
|
|
341
383
|
srtBuffer = Buffer.from(generateHeuristicSRT(text, audioBuffer.length), 'utf8');
|
|
342
384
|
fs.unlinkSync(outFile);
|
|
343
385
|
}
|
|
386
|
+
else if (engine === 'coqui') {
|
|
387
|
+
const url = this.getNodeParameter('coquiUrl', i);
|
|
388
|
+
const speaker = this.getNodeParameter('coquiSpeaker', i);
|
|
389
|
+
const lang = this.getNodeParameter('coquiLang', i);
|
|
390
|
+
const payload = {
|
|
391
|
+
text: text,
|
|
392
|
+
language_id: lang,
|
|
393
|
+
};
|
|
394
|
+
if (speaker)
|
|
395
|
+
payload.speaker_id = speaker;
|
|
396
|
+
const requestModule = url.startsWith('https') ? https : http;
|
|
397
|
+
audioBuffer = await new Promise((resolve, reject) => {
|
|
398
|
+
const req = requestModule.request(url, {
|
|
399
|
+
method: 'POST',
|
|
400
|
+
headers: {
|
|
401
|
+
'Content-Type': 'application/json',
|
|
402
|
+
}
|
|
403
|
+
}, (res) => {
|
|
404
|
+
const chunks = [];
|
|
405
|
+
res.on('data', (d) => chunks.push(d));
|
|
406
|
+
res.on('end', () => {
|
|
407
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
408
|
+
resolve(Buffer.concat(chunks));
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
reject(new Error(`Coqui Server Error ${res.statusCode}: ${Buffer.concat(chunks).toString()}`));
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
req.on('error', reject);
|
|
416
|
+
req.write(JSON.stringify(payload));
|
|
417
|
+
req.end();
|
|
418
|
+
});
|
|
419
|
+
srtBuffer = Buffer.from(generateHeuristicSRT(text, audioBuffer.length), 'utf8');
|
|
420
|
+
}
|
|
344
421
|
else {
|
|
345
422
|
const commandTpl = this.getNodeParameter('systemCommand', i);
|
|
346
423
|
const useClone = this.getNodeParameter('cloneInput', i, false);
|
|
@@ -402,7 +479,13 @@ class TTSBigBoss {
|
|
|
402
479
|
exports.TTSBigBoss = TTSBigBoss;
|
|
403
480
|
async function runEdgeTTS(text, voice, rate, pitch) {
|
|
404
481
|
return new Promise((resolve, reject) => {
|
|
405
|
-
const ws = new ws_1.default(EDGE_URL
|
|
482
|
+
const ws = new ws_1.default(EDGE_URL, {
|
|
483
|
+
headers: {
|
|
484
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
485
|
+
'Origin': 'chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold',
|
|
486
|
+
'TrustedClientToken': '6A5AA1D4EAFF4E9FB37E23D68491D6F4'
|
|
487
|
+
}
|
|
488
|
+
});
|
|
406
489
|
const requestId = (0, uuid_1.v4)().replace(/-/g, '');
|
|
407
490
|
const audioChunks = [];
|
|
408
491
|
const wordBoundaries = [];
|
|
@@ -601,6 +684,9 @@ async function ensurePiperModel(binDir, modelNameOrUrl) {
|
|
|
601
684
|
const configUrl = modelUrl + '.json';
|
|
602
685
|
console.log(`Downloading Piper Config: ${configUrl}`);
|
|
603
686
|
await downloadFile(configUrl, configPath);
|
|
687
|
+
if (!fs.existsSync(configPath)) {
|
|
688
|
+
throw new Error(`Failed to download config file: ${configPath}`);
|
|
689
|
+
}
|
|
604
690
|
try {
|
|
605
691
|
const content = fs.readFileSync(configPath, 'utf8');
|
|
606
692
|
JSON.parse(content);
|
|
@@ -609,7 +695,7 @@ async function ensurePiperModel(binDir, modelNameOrUrl) {
|
|
|
609
695
|
fs.unlinkSync(configPath);
|
|
610
696
|
if (fs.existsSync(modelPath))
|
|
611
697
|
fs.unlinkSync(modelPath);
|
|
612
|
-
throw new Error(`Downloaded config for ${modelNameOrUrl} was not valid JSON
|
|
698
|
+
throw new Error(`Downloaded config for ${modelNameOrUrl} was not valid JSON (likely 404 or network issue). Content start: ${fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8').substring(0, 50) : 'File missing'}`);
|
|
613
699
|
}
|
|
614
700
|
}
|
|
615
701
|
return { modelPath, configPath };
|
|
@@ -617,19 +703,36 @@ async function ensurePiperModel(binDir, modelNameOrUrl) {
|
|
|
617
703
|
async function downloadFile(url, dest) {
|
|
618
704
|
return new Promise((resolve, reject) => {
|
|
619
705
|
const file = fs.createWriteStream(dest);
|
|
620
|
-
|
|
706
|
+
file.on('error', (err) => {
|
|
707
|
+
fs.unlink(dest, () => { });
|
|
708
|
+
reject(new Error(`File write error: ${err.message}`));
|
|
709
|
+
});
|
|
710
|
+
const request = https.get(url, (response) => {
|
|
621
711
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
712
|
+
file.close();
|
|
622
713
|
downloadFile(response.headers.location, dest).then(resolve).catch(reject);
|
|
623
714
|
return;
|
|
624
715
|
}
|
|
716
|
+
if (response.statusCode && response.statusCode !== 200) {
|
|
717
|
+
file.close();
|
|
718
|
+
fs.unlink(dest, () => { });
|
|
719
|
+
reject(new Error(`Download failed with status code: ${response.statusCode} for URL: ${url}`));
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
625
722
|
response.pipe(file);
|
|
626
723
|
file.on('finish', () => {
|
|
627
|
-
file.close()
|
|
628
|
-
|
|
724
|
+
file.close((err) => {
|
|
725
|
+
if (err)
|
|
726
|
+
reject(err);
|
|
727
|
+
else
|
|
728
|
+
resolve();
|
|
729
|
+
});
|
|
629
730
|
});
|
|
630
|
-
})
|
|
731
|
+
});
|
|
732
|
+
request.on('error', (err) => {
|
|
733
|
+
file.close();
|
|
631
734
|
fs.unlink(dest, () => { });
|
|
632
|
-
reject(err);
|
|
735
|
+
reject(new Error(`Network error: ${err.message}`));
|
|
633
736
|
});
|
|
634
737
|
});
|
|
635
738
|
}
|
|
@@ -11,6 +11,7 @@ import * as os from 'os';
|
|
|
11
11
|
import * as child_process from 'child_process';
|
|
12
12
|
import WebSocket from 'ws';
|
|
13
13
|
import * as https from 'https';
|
|
14
|
+
import * as http from 'http'; // Added for Coqui HTTP support
|
|
14
15
|
import * as stream from 'stream';
|
|
15
16
|
import { promisify } from 'util';
|
|
16
17
|
import * as zlib from 'zlib'; // For extracting .tar.gz if needed, typically usage of tar command is easier on linux
|
|
@@ -18,40 +19,42 @@ import * as zlib from 'zlib'; // For extracting .tar.gz if needed, typically usa
|
|
|
18
19
|
const pipeline = promisify(stream.pipeline);
|
|
19
20
|
|
|
20
21
|
// Piper Models List (Curated High Quality)
|
|
22
|
+
// Note: Official Piper repo currently only has 'kareem' (Male) for Arabic.
|
|
23
|
+
// For Female Arabic voices, please use the 'Edge TTS' engine (Salma, Zariyah).
|
|
21
24
|
const PIPER_MODELS = [
|
|
22
25
|
// Arabic
|
|
23
|
-
{ name: 'Arabic (
|
|
24
|
-
{ name: 'Arabic (
|
|
26
|
+
{ name: 'Arabic (Jordan) - Kareem (Male) - Low', value: 'ar_JO-kareem-low' },
|
|
27
|
+
{ name: 'Arabic (Jordan) - Kareem (Male) - Medium', value: 'ar_JO-kareem-medium' },
|
|
25
28
|
|
|
26
29
|
// English (US)
|
|
27
|
-
{ name: 'English (US) - Lessac (Low
|
|
28
|
-
{ name: 'English (US) - Lessac (Medium
|
|
29
|
-
{ name: 'English (US) - Lessac (High
|
|
30
|
-
{ name: 'English (US) - Ryan (Low
|
|
31
|
-
{ name: 'English (US) - Ryan (Medium
|
|
32
|
-
{ name: 'English (US) - Ryan (High
|
|
33
|
-
{ name: 'English (US) - Amy (Low
|
|
34
|
-
{ name: 'English (US) - Amy (Medium
|
|
35
|
-
{ name: 'English (US) - Kathleen (Low
|
|
30
|
+
{ name: 'English (US) - Lessac (Female) - Low', value: 'en_US-lessac-low' },
|
|
31
|
+
{ name: 'English (US) - Lessac (Female) - Medium', value: 'en_US-lessac-medium' },
|
|
32
|
+
{ name: 'English (US) - Lessac (Female) - High', value: 'en_US-lessac-high' },
|
|
33
|
+
{ name: 'English (US) - Ryan (Male) - Low', value: 'en_US-ryan-low' },
|
|
34
|
+
{ name: 'English (US) - Ryan (Male) - Medium', value: 'en_US-ryan-medium' },
|
|
35
|
+
{ name: 'English (US) - Ryan (Male) - High', value: 'en_US-ryan-high' },
|
|
36
|
+
{ name: 'English (US) - Amy (Female) - Low', value: 'en_US-amy-low' },
|
|
37
|
+
{ name: 'English (US) - Amy (Female) - Medium', value: 'en_US-amy-medium' },
|
|
38
|
+
{ name: 'English (US) - Kathleen (Female) - Low', value: 'en_US-kathleen-low' },
|
|
36
39
|
|
|
37
40
|
// English (UK)
|
|
38
|
-
{ name: 'English (UK) - Alan (Low
|
|
39
|
-
{ name: 'English (UK) - Alan (Medium
|
|
40
|
-
{ name: 'English (UK) - Southern
|
|
41
|
+
{ name: 'English (UK) - Alan (Male) - Low', value: 'en_GB-alan-low' },
|
|
42
|
+
{ name: 'English (UK) - Alan (Male) - Medium', value: 'en_GB-alan-medium' },
|
|
43
|
+
{ name: 'English (UK) - Southern Female - Low', value: 'en_GB-southern_english_female-low' },
|
|
41
44
|
|
|
42
45
|
// French
|
|
43
|
-
{ name: 'French (
|
|
44
|
-
{ name: 'French (
|
|
46
|
+
{ name: 'French - Siwis (Female) - Low', value: 'fr_FR-siwis-low' },
|
|
47
|
+
{ name: 'French - Siwis (Female) - Medium', value: 'fr_FR-siwis-medium' },
|
|
45
48
|
|
|
46
49
|
// Spanish
|
|
47
|
-
{ name: 'Spanish (
|
|
48
|
-
{ name: 'Spanish (
|
|
50
|
+
{ name: 'Spanish (Spain) - Sharvard (Male) - Medium', value: 'es_ES-sharvard-medium' },
|
|
51
|
+
{ name: 'Spanish (Mexico) - Aldone (Male) - Medium', value: 'es_MX-aldona-medium' },
|
|
49
52
|
|
|
50
53
|
// German
|
|
51
|
-
{ name: 'German (
|
|
52
|
-
{ name: 'German (
|
|
53
|
-
{ name: 'German (
|
|
54
|
-
{ name: 'German (
|
|
54
|
+
{ name: 'German - Eva (Female) - High', value: 'de_DE-eva_k-x_low' },
|
|
55
|
+
{ name: 'German - Thorsten (Male) - High', value: 'de_DE-thorsten-high' },
|
|
56
|
+
{ name: 'German - Thorsten (Male) - Medium', value: 'de_DE-thorsten-medium' },
|
|
57
|
+
{ name: 'German - Thorsten (Male) - Low', value: 'de_DE-thorsten-low' },
|
|
55
58
|
];
|
|
56
59
|
|
|
57
60
|
// Edge TTS Constants
|
|
@@ -117,6 +120,11 @@ export class TTSBigBoss implements INodeType {
|
|
|
117
120
|
value: 'piper_local',
|
|
118
121
|
description: 'Downloads and runs Piper locally (Offline). Good quality, fast.',
|
|
119
122
|
},
|
|
123
|
+
{
|
|
124
|
+
name: 'Coqui TTS (Local Server)',
|
|
125
|
+
value: 'coqui',
|
|
126
|
+
description: 'Connect to a running Coqui TTS/XTTS server (e.g. python3 coqui_server.py).',
|
|
127
|
+
},
|
|
120
128
|
{
|
|
121
129
|
name: 'System Command (Custom)',
|
|
122
130
|
value: 'system',
|
|
@@ -262,7 +270,7 @@ export class TTSBigBoss implements INodeType {
|
|
|
262
270
|
engine: ['piper_local'],
|
|
263
271
|
},
|
|
264
272
|
},
|
|
265
|
-
description: 'Select a voice model. It will be downloaded automatically
|
|
273
|
+
description: 'Select a voice model. It will be downloaded automatically. Note: Official Piper Arabic only has "Kareem" (Male). For Female Arabic, please use the "Edge TTS" engine.',
|
|
266
274
|
},
|
|
267
275
|
{
|
|
268
276
|
displayName: 'Custom Model Name/URL',
|
|
@@ -278,6 +286,45 @@ export class TTSBigBoss implements INodeType {
|
|
|
278
286
|
},
|
|
279
287
|
description: 'Name from Hugging Face (e.g. en_US-bryce-medium) or full URL to .onnx file.',
|
|
280
288
|
},
|
|
289
|
+
// ----------------------------------
|
|
290
|
+
// Coqui Server Settings
|
|
291
|
+
// ----------------------------------
|
|
292
|
+
{
|
|
293
|
+
displayName: 'Server URL',
|
|
294
|
+
name: 'coquiUrl',
|
|
295
|
+
type: 'string',
|
|
296
|
+
default: 'http://localhost:5002/api/tts',
|
|
297
|
+
description: 'URL of the running Coqui server endpoint (usually /api/tts or /tts_stream).',
|
|
298
|
+
displayOptions: {
|
|
299
|
+
show: {
|
|
300
|
+
engine: ['coqui'],
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
displayName: 'Speaker ID / Wav Path',
|
|
306
|
+
name: 'coquiSpeaker',
|
|
307
|
+
type: 'string',
|
|
308
|
+
default: '',
|
|
309
|
+
description: 'Speaker ID (if multi-speaker) or path to reference wav (for cloning).',
|
|
310
|
+
displayOptions: {
|
|
311
|
+
show: {
|
|
312
|
+
engine: ['coqui'],
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
displayName: 'Language',
|
|
318
|
+
name: 'coquiLang',
|
|
319
|
+
type: 'string',
|
|
320
|
+
default: 'en',
|
|
321
|
+
description: 'Language code (e.g. en, ar, fr).',
|
|
322
|
+
displayOptions: {
|
|
323
|
+
show: {
|
|
324
|
+
engine: ['coqui'],
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
},
|
|
281
328
|
],
|
|
282
329
|
};
|
|
283
330
|
|
|
@@ -375,6 +422,49 @@ export class TTSBigBoss implements INodeType {
|
|
|
375
422
|
|
|
376
423
|
fs.unlinkSync(outFile);
|
|
377
424
|
|
|
425
|
+
} else if (engine === 'coqui') {
|
|
426
|
+
// ----------------------------------
|
|
427
|
+
// COQUI SEVER EXECUTION
|
|
428
|
+
// ----------------------------------
|
|
429
|
+
const url = this.getNodeParameter('coquiUrl', i) as string;
|
|
430
|
+
const speaker = this.getNodeParameter('coquiSpeaker', i) as string;
|
|
431
|
+
const lang = this.getNodeParameter('coquiLang', i) as string;
|
|
432
|
+
|
|
433
|
+
// Construct Payload
|
|
434
|
+
// Standard XTTS/Coqui API expects: text, speaker_id, language_id
|
|
435
|
+
const payload: any = {
|
|
436
|
+
text: text,
|
|
437
|
+
language_id: lang,
|
|
438
|
+
};
|
|
439
|
+
if (speaker) payload.speaker_id = speaker;
|
|
440
|
+
|
|
441
|
+
// Allow http and https
|
|
442
|
+
const requestModule = url.startsWith('https') ? https : http;
|
|
443
|
+
|
|
444
|
+
audioBuffer = await new Promise((resolve, reject) => {
|
|
445
|
+
const req = requestModule.request(url, {
|
|
446
|
+
method: 'POST',
|
|
447
|
+
headers: {
|
|
448
|
+
'Content-Type': 'application/json',
|
|
449
|
+
}
|
|
450
|
+
}, (res: any) => {
|
|
451
|
+
const chunks: any[] = [];
|
|
452
|
+
res.on('data', (d: any) => chunks.push(d));
|
|
453
|
+
res.on('end', () => {
|
|
454
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
455
|
+
resolve(Buffer.concat(chunks));
|
|
456
|
+
} else {
|
|
457
|
+
reject(new Error(`Coqui Server Error ${res.statusCode}: ${Buffer.concat(chunks).toString()}`));
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
req.on('error', reject);
|
|
462
|
+
req.write(JSON.stringify(payload));
|
|
463
|
+
req.end();
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
srtBuffer = Buffer.from(generateHeuristicSRT(text, audioBuffer.length), 'utf8');
|
|
467
|
+
|
|
378
468
|
} else {
|
|
379
469
|
// ----------------------------------
|
|
380
470
|
// SYSTEM COMMAND EXECUTION
|
|
@@ -468,7 +558,13 @@ export class TTSBigBoss implements INodeType {
|
|
|
468
558
|
// --------------------------------------------------------------------------
|
|
469
559
|
async function runEdgeTTS(text: string, voice: string, rate: string, pitch: string): Promise<{ audio: Buffer; srt: string }> {
|
|
470
560
|
return new Promise((resolve, reject) => {
|
|
471
|
-
const ws = new WebSocket(EDGE_URL
|
|
561
|
+
const ws = new WebSocket(EDGE_URL, {
|
|
562
|
+
headers: {
|
|
563
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
564
|
+
'Origin': 'chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold',
|
|
565
|
+
'TrustedClientToken': '6A5AA1D4EAFF4E9FB37E23D68491D6F4'
|
|
566
|
+
}
|
|
567
|
+
});
|
|
472
568
|
const requestId = uuidv4().replace(/-/g, '');
|
|
473
569
|
const audioChunks: Buffer[] = [];
|
|
474
570
|
const wordBoundaries: WordBoundary[] = [];
|
|
@@ -753,13 +849,16 @@ async function ensurePiperModel(binDir: string, modelNameOrUrl: string): Promise
|
|
|
753
849
|
await downloadFile(configUrl, configPath);
|
|
754
850
|
|
|
755
851
|
// Validate JSON
|
|
852
|
+
if (!fs.existsSync(configPath)) {
|
|
853
|
+
throw new Error(`Failed to download config file: ${configPath}`);
|
|
854
|
+
}
|
|
756
855
|
try {
|
|
757
856
|
const content = fs.readFileSync(configPath, 'utf8');
|
|
758
857
|
JSON.parse(content);
|
|
759
858
|
} catch (e) {
|
|
760
859
|
fs.unlinkSync(configPath); // Delete bad file
|
|
761
860
|
if (fs.existsSync(modelPath)) fs.unlinkSync(modelPath); // Delete model too as it might be bad
|
|
762
|
-
throw new Error(`Downloaded config for ${modelNameOrUrl} was not valid JSON
|
|
861
|
+
throw new Error(`Downloaded config for ${modelNameOrUrl} was not valid JSON (likely 404 or network issue). Content start: ${fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8').substring(0, 50) : 'File missing'}`);
|
|
763
862
|
}
|
|
764
863
|
}
|
|
765
864
|
|
|
@@ -769,20 +868,42 @@ async function ensurePiperModel(binDir: string, modelNameOrUrl: string): Promise
|
|
|
769
868
|
async function downloadFile(url: string, dest: string): Promise<void> {
|
|
770
869
|
return new Promise((resolve, reject) => {
|
|
771
870
|
const file = fs.createWriteStream(dest);
|
|
772
|
-
|
|
871
|
+
|
|
872
|
+
// Handle file system errors (e.g. permissions)
|
|
873
|
+
file.on('error', (err) => {
|
|
874
|
+
fs.unlink(dest, () => { }); // Cleanup
|
|
875
|
+
reject(new Error(`File write error: ${err.message}`));
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
const request = https.get(url, (response) => {
|
|
773
879
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
774
880
|
// Follow redirect
|
|
881
|
+
file.close();
|
|
775
882
|
downloadFile(response.headers.location!, dest).then(resolve).catch(reject);
|
|
776
883
|
return;
|
|
777
884
|
}
|
|
885
|
+
|
|
886
|
+
if (response.statusCode && response.statusCode !== 200) {
|
|
887
|
+
file.close();
|
|
888
|
+
fs.unlink(dest, () => { });
|
|
889
|
+
reject(new Error(`Download failed with status code: ${response.statusCode} for URL: ${url}`));
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
|
|
778
893
|
response.pipe(file);
|
|
894
|
+
|
|
779
895
|
file.on('finish', () => {
|
|
780
|
-
file.close()
|
|
781
|
-
|
|
896
|
+
file.close((err) => {
|
|
897
|
+
if (err) reject(err);
|
|
898
|
+
else resolve();
|
|
899
|
+
});
|
|
782
900
|
});
|
|
783
|
-
})
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
request.on('error', (err) => {
|
|
904
|
+
file.close();
|
|
784
905
|
fs.unlink(dest, () => { });
|
|
785
|
-
reject(err);
|
|
906
|
+
reject(new Error(`Network error: ${err.message}`));
|
|
786
907
|
});
|
|
787
908
|
});
|
|
788
909
|
}
|