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.
@@ -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 (ar_JO) - Kareem (Low)', value: 'ar_JO-kareem-low' },
52
- { name: 'Arabic (ar_JO) - Kareem (Medium)', value: 'ar_JO-kareem-medium' },
53
- { name: 'English (US) - Lessac (Low)', value: 'en_US-lessac-low' },
54
- { name: 'English (US) - Lessac (Medium)', value: 'en_US-lessac-medium' },
55
- { name: 'English (US) - Lessac (High)', value: 'en_US-lessac-high' },
56
- { name: 'English (US) - Ryan (Low)', value: 'en_US-ryan-low' },
57
- { name: 'English (US) - Ryan (Medium)', value: 'en_US-ryan-medium' },
58
- { name: 'English (US) - Ryan (High)', value: 'en_US-ryan-high' },
59
- { name: 'English (US) - Amy (Low)', value: 'en_US-amy-low' },
60
- { name: 'English (US) - Amy (Medium)', value: 'en_US-amy-medium' },
61
- { name: 'English (US) - Kathleen (Low)', value: 'en_US-kathleen-low' },
62
- { name: 'English (UK) - Alan (Low)', value: 'en_GB-alan-low' },
63
- { name: 'English (UK) - Alan (Medium)', value: 'en_GB-alan-medium' },
64
- { name: 'English (UK) - Southern English Female (Low)', value: 'en_GB-southern_english_female-low' },
65
- { name: 'French (fr_FR) - Siwis (Low)', value: 'fr_FR-siwis-low' },
66
- { name: 'French (fr_FR) - Siwis (Medium)', value: 'fr_FR-siwis-medium' },
67
- { name: 'Spanish (es_ES) - Sharvard (Medium)', value: 'es_ES-sharvard-medium' },
68
- { name: 'Spanish (es_MX) - Aldone (Medium)', value: 'es_MX-aldona-medium' },
69
- { name: 'German (de_DE) - Eva (High)', value: 'de_DE-eva_k-x_low' },
70
- { name: 'German (de_DE) - Thorsten (High)', value: 'de_DE-thorsten-high' },
71
- { name: 'German (de_DE) - Thorsten (Medium)', value: 'de_DE-thorsten-medium' },
72
- { name: 'German (de_DE) - Thorsten (Low)', value: 'de_DE-thorsten-low' },
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 on first use.',
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. 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
@@ -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 (ar_JO) - Kareem (Low)', value: 'ar_JO-kareem-low' },
24
- { name: 'Arabic (ar_JO) - Kareem (Medium)', value: 'ar_JO-kareem-medium' },
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)', value: 'en_US-lessac-low' },
28
- { name: 'English (US) - Lessac (Medium)', value: 'en_US-lessac-medium' },
29
- { name: 'English (US) - Lessac (High)', value: 'en_US-lessac-high' },
30
- { name: 'English (US) - Ryan (Low)', value: 'en_US-ryan-low' },
31
- { name: 'English (US) - Ryan (Medium)', value: 'en_US-ryan-medium' },
32
- { name: 'English (US) - Ryan (High)', value: 'en_US-ryan-high' },
33
- { name: 'English (US) - Amy (Low)', value: 'en_US-amy-low' },
34
- { name: 'English (US) - Amy (Medium)', value: 'en_US-amy-medium' },
35
- { name: 'English (US) - Kathleen (Low)', value: 'en_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)', value: 'en_GB-alan-low' },
39
- { name: 'English (UK) - Alan (Medium)', value: 'en_GB-alan-medium' },
40
- { name: 'English (UK) - Southern English Female (Low)', value: 'en_GB-southern_english_female-low' },
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 (fr_FR) - Siwis (Low)', value: 'fr_FR-siwis-low' },
44
- { name: 'French (fr_FR) - Siwis (Medium)', value: 'fr_FR-siwis-medium' },
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 (es_ES) - Sharvard (Medium)', value: 'es_ES-sharvard-medium' },
48
- { name: 'Spanish (es_MX) - Aldone (Medium)', value: 'es_MX-aldona-medium' },
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 (de_DE) - Eva (High)', value: 'de_DE-eva_k-x_low' }, // mapped to available
52
- { name: 'German (de_DE) - Thorsten (High)', value: 'de_DE-thorsten-high' },
53
- { name: 'German (de_DE) - Thorsten (Medium)', value: 'de_DE-thorsten-medium' },
54
- { name: 'German (de_DE) - Thorsten (Low)', value: 'de_DE-thorsten-low' },
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 on first use.',
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. 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'}`);
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
- 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) => {
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
- resolve();
896
+ file.close((err) => {
897
+ if (err) reject(err);
898
+ else resolve();
899
+ });
782
900
  });
783
- }).on('error', (err) => {
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-tts-bigboss",
3
- "version": "1.0.3",
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",