n8n-nodes-tts-bigboss 1.0.4 → 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 +110 -7
- package/nodes/TTSBigBoss/TTSBigBoss.node.ts +126 -7
- package/package.json +1 -1
package/dist/TTSBigBoss.node.js
CHANGED
|
@@ -44,6 +44,7 @@ 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);
|
|
@@ -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',
|
|
@@ -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
|
|
@@ -119,6 +120,11 @@ export class TTSBigBoss implements INodeType {
|
|
|
119
120
|
value: 'piper_local',
|
|
120
121
|
description: 'Downloads and runs Piper locally (Offline). Good quality, fast.',
|
|
121
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
|
+
},
|
|
122
128
|
{
|
|
123
129
|
name: 'System Command (Custom)',
|
|
124
130
|
value: 'system',
|
|
@@ -280,6 +286,45 @@ export class TTSBigBoss implements INodeType {
|
|
|
280
286
|
},
|
|
281
287
|
description: 'Name from Hugging Face (e.g. en_US-bryce-medium) or full URL to .onnx file.',
|
|
282
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
|
+
},
|
|
283
328
|
],
|
|
284
329
|
};
|
|
285
330
|
|
|
@@ -377,6 +422,49 @@ export class TTSBigBoss implements INodeType {
|
|
|
377
422
|
|
|
378
423
|
fs.unlinkSync(outFile);
|
|
379
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
|
+
|
|
380
468
|
} else {
|
|
381
469
|
// ----------------------------------
|
|
382
470
|
// SYSTEM COMMAND EXECUTION
|
|
@@ -470,7 +558,13 @@ export class TTSBigBoss implements INodeType {
|
|
|
470
558
|
// --------------------------------------------------------------------------
|
|
471
559
|
async function runEdgeTTS(text: string, voice: string, rate: string, pitch: string): Promise<{ audio: Buffer; srt: string }> {
|
|
472
560
|
return new Promise((resolve, reject) => {
|
|
473
|
-
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
|
+
});
|
|
474
568
|
const requestId = uuidv4().replace(/-/g, '');
|
|
475
569
|
const audioChunks: Buffer[] = [];
|
|
476
570
|
const wordBoundaries: WordBoundary[] = [];
|
|
@@ -755,13 +849,16 @@ async function ensurePiperModel(binDir: string, modelNameOrUrl: string): Promise
|
|
|
755
849
|
await downloadFile(configUrl, configPath);
|
|
756
850
|
|
|
757
851
|
// Validate JSON
|
|
852
|
+
if (!fs.existsSync(configPath)) {
|
|
853
|
+
throw new Error(`Failed to download config file: ${configPath}`);
|
|
854
|
+
}
|
|
758
855
|
try {
|
|
759
856
|
const content = fs.readFileSync(configPath, 'utf8');
|
|
760
857
|
JSON.parse(content);
|
|
761
858
|
} catch (e) {
|
|
762
859
|
fs.unlinkSync(configPath); // Delete bad file
|
|
763
860
|
if (fs.existsSync(modelPath)) fs.unlinkSync(modelPath); // Delete model too as it might be bad
|
|
764
|
-
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'}`);
|
|
765
862
|
}
|
|
766
863
|
}
|
|
767
864
|
|
|
@@ -771,20 +868,42 @@ async function ensurePiperModel(binDir: string, modelNameOrUrl: string): Promise
|
|
|
771
868
|
async function downloadFile(url: string, dest: string): Promise<void> {
|
|
772
869
|
return new Promise((resolve, reject) => {
|
|
773
870
|
const file = fs.createWriteStream(dest);
|
|
774
|
-
|
|
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) => {
|
|
775
879
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
776
880
|
// Follow redirect
|
|
881
|
+
file.close();
|
|
777
882
|
downloadFile(response.headers.location!, dest).then(resolve).catch(reject);
|
|
778
883
|
return;
|
|
779
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
|
+
|
|
780
893
|
response.pipe(file);
|
|
894
|
+
|
|
781
895
|
file.on('finish', () => {
|
|
782
|
-
file.close()
|
|
783
|
-
|
|
896
|
+
file.close((err) => {
|
|
897
|
+
if (err) reject(err);
|
|
898
|
+
else resolve();
|
|
899
|
+
});
|
|
784
900
|
});
|
|
785
|
-
})
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
request.on('error', (err) => {
|
|
904
|
+
file.close();
|
|
786
905
|
fs.unlink(dest, () => { });
|
|
787
|
-
reject(err);
|
|
906
|
+
reject(new Error(`Network error: ${err.message}`));
|
|
788
907
|
});
|
|
789
908
|
});
|
|
790
909
|
}
|