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.
@@ -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. URL might be wrong: ${configUrl}. Content start: ${fs.readFileSync(configPath, 'utf8').substring(0, 50)}...`);
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
- https.get(url, (response) => {
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
- resolve();
724
+ file.close((err) => {
725
+ if (err)
726
+ reject(err);
727
+ else
728
+ resolve();
729
+ });
629
730
  });
630
- }).on('error', (err) => {
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. URL might be wrong: ${configUrl}. Content start: ${fs.readFileSync(configPath, 'utf8').substring(0, 50)}...`);
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
- https.get(url, (response) => {
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
- resolve();
896
+ file.close((err) => {
897
+ if (err) reject(err);
898
+ else resolve();
899
+ });
784
900
  });
785
- }).on('error', (err) => {
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-tts-bigboss",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "BigBoss TTS node with multi-engine support and automatic SRT generation",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",