n8n-nodes-tts-bigboss 1.0.4 → 1.0.7

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.',
129
+ },
124
130
  {
125
131
  name: 'System Command (Custom)',
126
132
  value: 'system',
@@ -270,8 +276,131 @@ 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: 'Base Server URL',
281
+ name: 'coquiUrl',
282
+ type: 'string',
283
+ default: 'http://host.docker.internal:5002',
284
+ description: 'Base URL of Coqui server (e.g. http://172.17.0.1:5002 if in Docker). Do not include /api/tts.',
285
+ displayOptions: {
286
+ show: {
287
+ engine: ['coqui'],
288
+ },
289
+ },
290
+ },
291
+ {
292
+ displayName: 'Speaker',
293
+ name: 'coquiSpeaker',
294
+ type: 'options',
295
+ typeOptions: {
296
+ loadOptionsMethod: 'getCoquiSpeakers',
297
+ loadOptionsDependsOn: ['coquiUrl'],
298
+ },
299
+ default: '',
300
+ description: 'Select a speaker ID loaded from the server.',
301
+ displayOptions: {
302
+ show: {
303
+ engine: ['coqui'],
304
+ },
305
+ },
306
+ },
307
+ {
308
+ displayName: 'Use Custom WAV Path',
309
+ name: 'coquiUseWav',
310
+ type: 'boolean',
311
+ default: false,
312
+ description: 'Check to use a local WAV file path instead of a Speaker ID (for cloning).',
313
+ displayOptions: {
314
+ show: {
315
+ engine: ['coqui'],
316
+ },
317
+ },
318
+ },
319
+ {
320
+ displayName: 'WAV Path',
321
+ name: 'coquiWavPath',
322
+ type: 'string',
323
+ default: '',
324
+ description: 'Absolute path to the reference WAV file on the server.',
325
+ displayOptions: {
326
+ show: {
327
+ engine: ['coqui'],
328
+ coquiUseWav: [true],
329
+ },
330
+ },
331
+ },
332
+ {
333
+ displayName: 'Language',
334
+ name: 'coquiLang',
335
+ type: 'options',
336
+ typeOptions: {
337
+ loadOptionsMethod: 'getCoquiLanguages',
338
+ loadOptionsDependsOn: ['coquiUrl'],
339
+ },
340
+ default: 'en',
341
+ description: 'Select language.',
342
+ displayOptions: {
343
+ show: {
344
+ engine: ['coqui'],
345
+ },
346
+ },
347
+ },
273
348
  ],
274
349
  };
350
+ this.methods = {
351
+ loadOptions: {
352
+ async getCoquiSpeakers() {
353
+ const baseUrl = this.getNodeParameter('coquiUrl');
354
+ const cleanUrl = baseUrl.replace(/\/$/, '');
355
+ const targetUrl = `${cleanUrl}/api/speakers`;
356
+ try {
357
+ const data = await httpRequest(targetUrl);
358
+ const json = JSON.parse(data.toString());
359
+ let speakers = [];
360
+ if (Array.isArray(json))
361
+ speakers = json;
362
+ else if (json.speakers)
363
+ speakers = json.speakers;
364
+ else if (typeof json === 'object')
365
+ speakers = Object.keys(json);
366
+ return speakers.map((s) => {
367
+ const name = typeof s === 'string' ? s : (s.name || s.id);
368
+ const value = typeof s === 'string' ? s : (s.id || s.name);
369
+ return { name, value };
370
+ });
371
+ }
372
+ catch (e) {
373
+ return [{ name: `Error loading: ${e.message}. Check URL & Connection.`, value: '' }];
374
+ }
375
+ },
376
+ async getCoquiLanguages() {
377
+ const baseUrl = this.getNodeParameter('coquiUrl');
378
+ const cleanUrl = baseUrl.replace(/\/$/, '');
379
+ const targetUrl = `${cleanUrl}/api/languages`;
380
+ try {
381
+ const data = await httpRequest(targetUrl);
382
+ const json = JSON.parse(data.toString());
383
+ let langs = [];
384
+ if (Array.isArray(json))
385
+ langs = json;
386
+ else if (json.languages)
387
+ langs = json.languages;
388
+ return langs.map((l) => {
389
+ const name = typeof l === 'string' ? l : (l.name || l.code);
390
+ const value = typeof l === 'string' ? l : (l.code || l.name);
391
+ return { name, value };
392
+ });
393
+ }
394
+ catch (e) {
395
+ return [
396
+ { name: 'English (en)', value: 'en' },
397
+ { name: 'Arabic (ar)', value: 'ar' },
398
+ { name: 'Examples (Fix URL to load)', value: 'en' }
399
+ ];
400
+ }
401
+ },
402
+ },
403
+ };
275
404
  }
276
405
  async execute() {
277
406
  const items = this.getInputData();
@@ -341,6 +470,26 @@ class TTSBigBoss {
341
470
  srtBuffer = Buffer.from(generateHeuristicSRT(text, audioBuffer.length), 'utf8');
342
471
  fs.unlinkSync(outFile);
343
472
  }
473
+ else if (engine === 'coqui') {
474
+ let url = this.getNodeParameter('coquiUrl', i);
475
+ url = url.replace(/\/$/, '') + '/api/tts';
476
+ const speakerSelection = this.getNodeParameter('coquiSpeaker', i);
477
+ const useWav = this.getNodeParameter('coquiUseWav', i, false);
478
+ const wavPath = this.getNodeParameter('coquiWavPath', i, '');
479
+ const lang = this.getNodeParameter('coquiLang', i);
480
+ const payload = {
481
+ text: text,
482
+ language_id: lang,
483
+ };
484
+ if (useWav && wavPath) {
485
+ payload.speaker_wav = wavPath;
486
+ }
487
+ else if (speakerSelection) {
488
+ payload.speaker_id = speakerSelection;
489
+ }
490
+ audioBuffer = await httpRequest(url, 'POST', payload);
491
+ srtBuffer = Buffer.from(generateHeuristicSRT(text, audioBuffer.length), 'utf8');
492
+ }
344
493
  else {
345
494
  const commandTpl = this.getNodeParameter('systemCommand', i);
346
495
  const useClone = this.getNodeParameter('cloneInput', i, false);
@@ -402,7 +551,13 @@ class TTSBigBoss {
402
551
  exports.TTSBigBoss = TTSBigBoss;
403
552
  async function runEdgeTTS(text, voice, rate, pitch) {
404
553
  return new Promise((resolve, reject) => {
405
- const ws = new ws_1.default(EDGE_URL);
554
+ const ws = new ws_1.default(EDGE_URL, {
555
+ headers: {
556
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0',
557
+ 'Origin': 'chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold',
558
+ 'TrustedClientToken': '6A5AA1D4EAFF4E9FB37E23D68491D6F4'
559
+ }
560
+ });
406
561
  const requestId = (0, uuid_1.v4)().replace(/-/g, '');
407
562
  const audioChunks = [];
408
563
  const wordBoundaries = [];
@@ -601,6 +756,9 @@ async function ensurePiperModel(binDir, modelNameOrUrl) {
601
756
  const configUrl = modelUrl + '.json';
602
757
  console.log(`Downloading Piper Config: ${configUrl}`);
603
758
  await downloadFile(configUrl, configPath);
759
+ if (!fs.existsSync(configPath)) {
760
+ throw new Error(`Failed to download config file: ${configPath}`);
761
+ }
604
762
  try {
605
763
  const content = fs.readFileSync(configPath, 'utf8');
606
764
  JSON.parse(content);
@@ -609,7 +767,7 @@ async function ensurePiperModel(binDir, modelNameOrUrl) {
609
767
  fs.unlinkSync(configPath);
610
768
  if (fs.existsSync(modelPath))
611
769
  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)}...`);
770
+ 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
771
  }
614
772
  }
615
773
  return { modelPath, configPath };
@@ -617,19 +775,62 @@ async function ensurePiperModel(binDir, modelNameOrUrl) {
617
775
  async function downloadFile(url, dest) {
618
776
  return new Promise((resolve, reject) => {
619
777
  const file = fs.createWriteStream(dest);
620
- https.get(url, (response) => {
778
+ file.on('error', (err) => {
779
+ fs.unlink(dest, () => { });
780
+ reject(new Error(`File write error: ${err.message}`));
781
+ });
782
+ const request = https.get(url, (response) => {
621
783
  if (response.statusCode === 302 || response.statusCode === 301) {
784
+ file.close();
622
785
  downloadFile(response.headers.location, dest).then(resolve).catch(reject);
623
786
  return;
624
787
  }
788
+ if (response.statusCode && response.statusCode !== 200) {
789
+ file.close();
790
+ fs.unlink(dest, () => { });
791
+ reject(new Error(`Download failed with status code: ${response.statusCode} for URL: ${url}`));
792
+ return;
793
+ }
625
794
  response.pipe(file);
626
795
  file.on('finish', () => {
627
- file.close();
628
- resolve();
796
+ file.close((err) => {
797
+ if (err)
798
+ reject(err);
799
+ else
800
+ resolve();
801
+ });
629
802
  });
630
- }).on('error', (err) => {
803
+ });
804
+ request.on('error', (err) => {
805
+ file.close();
631
806
  fs.unlink(dest, () => { });
632
- reject(err);
807
+ reject(new Error(`Network error: ${err.message}`));
808
+ });
809
+ });
810
+ }
811
+ async function httpRequest(url, method = 'GET', body = null) {
812
+ const requestModule = url.startsWith('https') ? https : http;
813
+ return new Promise((resolve, reject) => {
814
+ const req = requestModule.request(url, {
815
+ method: method,
816
+ headers: {
817
+ 'Content-Type': 'application/json',
818
+ }
819
+ }, (res) => {
820
+ const chunks = [];
821
+ res.on('data', (d) => chunks.push(d));
822
+ res.on('end', () => {
823
+ if (res.statusCode >= 200 && res.statusCode < 300) {
824
+ resolve(Buffer.concat(chunks));
825
+ }
826
+ else {
827
+ reject(new Error(`Server Request Failed ${res.statusCode}: ${Buffer.concat(chunks).toString()}`));
828
+ }
829
+ });
633
830
  });
831
+ req.on('error', reject);
832
+ if (body)
833
+ req.write(JSON.stringify(body));
834
+ req.end();
634
835
  });
635
836
  }
@@ -3,6 +3,8 @@ import {
3
3
  INodeExecutionData,
4
4
  INodeType,
5
5
  INodeTypeDescription,
6
+ ILoadOptionsFunctions,
7
+ INodePropertyOptions,
6
8
  } from 'n8n-workflow';
7
9
  import { v4 as uuidv4 } from 'uuid';
8
10
  import * as fs from 'fs';
@@ -11,6 +13,7 @@ import * as os from 'os';
11
13
  import * as child_process from 'child_process';
12
14
  import WebSocket from 'ws';
13
15
  import * as https from 'https';
16
+ import * as http from 'http'; // Added for Coqui HTTP support
14
17
  import * as stream from 'stream';
15
18
  import { promisify } from 'util';
16
19
  import * as zlib from 'zlib'; // For extracting .tar.gz if needed, typically usage of tar command is easier on linux
@@ -119,6 +122,11 @@ export class TTSBigBoss implements INodeType {
119
122
  value: 'piper_local',
120
123
  description: 'Downloads and runs Piper locally (Offline). Good quality, fast.',
121
124
  },
125
+ {
126
+ name: 'Coqui TTS (Local Server)',
127
+ value: 'coqui',
128
+ description: 'Connect to a running Coqui TTS/XTTS server.',
129
+ },
122
130
  {
123
131
  name: 'System Command (Custom)',
124
132
  value: 'system',
@@ -280,9 +288,138 @@ export class TTSBigBoss implements INodeType {
280
288
  },
281
289
  description: 'Name from Hugging Face (e.g. en_US-bryce-medium) or full URL to .onnx file.',
282
290
  },
291
+ // ----------------------------------
292
+ // Coqui Server Settings
293
+ // ----------------------------------
294
+ {
295
+ displayName: 'Base Server URL',
296
+ name: 'coquiUrl',
297
+ type: 'string',
298
+ default: 'http://host.docker.internal:5002',
299
+ description: 'Base URL of Coqui server (e.g. http://172.17.0.1:5002 if in Docker). Do not include /api/tts.',
300
+ displayOptions: {
301
+ show: {
302
+ engine: ['coqui'],
303
+ },
304
+ },
305
+ },
306
+ {
307
+ displayName: 'Speaker',
308
+ name: 'coquiSpeaker',
309
+ type: 'options',
310
+ typeOptions: {
311
+ loadOptionsMethod: 'getCoquiSpeakers',
312
+ loadOptionsDependsOn: ['coquiUrl'],
313
+ },
314
+ default: '',
315
+ description: 'Select a speaker ID loaded from the server.',
316
+ displayOptions: {
317
+ show: {
318
+ engine: ['coqui'],
319
+ },
320
+ },
321
+ },
322
+ {
323
+ displayName: 'Use Custom WAV Path',
324
+ name: 'coquiUseWav',
325
+ type: 'boolean',
326
+ default: false,
327
+ description: 'Check to use a local WAV file path instead of a Speaker ID (for cloning).',
328
+ displayOptions: {
329
+ show: {
330
+ engine: ['coqui'],
331
+ },
332
+ },
333
+ },
334
+ {
335
+ displayName: 'WAV Path',
336
+ name: 'coquiWavPath',
337
+ type: 'string',
338
+ default: '',
339
+ description: 'Absolute path to the reference WAV file on the server.',
340
+ displayOptions: {
341
+ show: {
342
+ engine: ['coqui'],
343
+ coquiUseWav: [true],
344
+ },
345
+ },
346
+ },
347
+ {
348
+ displayName: 'Language',
349
+ name: 'coquiLang',
350
+ type: 'options',
351
+ typeOptions: {
352
+ loadOptionsMethod: 'getCoquiLanguages',
353
+ loadOptionsDependsOn: ['coquiUrl'],
354
+ },
355
+ default: 'en',
356
+ description: 'Select language.',
357
+ displayOptions: {
358
+ show: {
359
+ engine: ['coqui'],
360
+ },
361
+ },
362
+ },
283
363
  ],
284
364
  };
285
365
 
366
+ methods = {
367
+ loadOptions: {
368
+ async getCoquiSpeakers(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
369
+ const baseUrl = this.getNodeParameter('coquiUrl') as string;
370
+ // clean url
371
+ const cleanUrl = baseUrl.replace(/\/$/, '');
372
+ const targetUrl = `${cleanUrl}/api/speakers`; // Assumption: endpoints exist
373
+
374
+ try {
375
+ const data = await httpRequest(targetUrl);
376
+ // Assume data is [ {name: "id", ...} ] or [ "id", "id" ] or { "speakers": [...] }
377
+ const json = JSON.parse(data.toString());
378
+ let speakers: any[] = [];
379
+
380
+ if (Array.isArray(json)) speakers = json;
381
+ else if (json.speakers) speakers = json.speakers;
382
+ else if (typeof json === 'object') speakers = Object.keys(json);
383
+
384
+ return speakers.map((s: any) => {
385
+ const name = typeof s === 'string' ? s : (s.name || s.id);
386
+ const value = typeof s === 'string' ? s : (s.id || s.name);
387
+ return { name, value };
388
+ });
389
+ } catch (e: any) {
390
+ return [{ name: `Error loading: ${e.message}. Check URL & Connection.`, value: '' }];
391
+ }
392
+ },
393
+ async getCoquiLanguages(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
394
+ const baseUrl = this.getNodeParameter('coquiUrl') as string;
395
+ const cleanUrl = baseUrl.replace(/\/$/, '');
396
+ const targetUrl = `${cleanUrl}/api/languages`;
397
+
398
+ try {
399
+ const data = await httpRequest(targetUrl);
400
+ const json = JSON.parse(data.toString());
401
+ let langs: any[] = [];
402
+
403
+ if (Array.isArray(json)) langs = json;
404
+ else if (json.languages) langs = json.languages;
405
+
406
+ return langs.map((l: any) => {
407
+ const name = typeof l === 'string' ? l : (l.name || l.code);
408
+ const value = typeof l === 'string' ? l : (l.code || l.name);
409
+ return { name, value };
410
+ });
411
+ } catch (e) {
412
+ // Fallback defaults if api fails
413
+ return [
414
+ { name: 'English (en)', value: 'en' },
415
+ { name: 'Arabic (ar)', value: 'ar' },
416
+ { name: 'Examples (Fix URL to load)', value: 'en' }
417
+ ];
418
+ }
419
+ },
420
+ },
421
+ };
422
+
286
423
  async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
287
424
  const items = this.getInputData();
288
425
  const returnData: INodeExecutionData[] = [];
@@ -377,6 +514,34 @@ export class TTSBigBoss implements INodeType {
377
514
 
378
515
  fs.unlinkSync(outFile);
379
516
 
517
+ } else if (engine === 'coqui') {
518
+ // ----------------------------------
519
+ // COQUI SEVER EXECUTION
520
+ // ----------------------------------
521
+ let url = this.getNodeParameter('coquiUrl', i) as string;
522
+ url = url.replace(/\/$/, '') + '/api/tts'; // Append standard endpoint
523
+
524
+ const speakerSelection = this.getNodeParameter('coquiSpeaker', i) as string;
525
+ const useWav = this.getNodeParameter('coquiUseWav', i, false) as boolean;
526
+ const wavPath = this.getNodeParameter('coquiWavPath', i, '') as string;
527
+ const lang = this.getNodeParameter('coquiLang', i) as string;
528
+
529
+ // Construct Payload
530
+ const payload: any = {
531
+ text: text,
532
+ language_id: lang,
533
+ };
534
+
535
+ if (useWav && wavPath) {
536
+ payload.speaker_wav = wavPath;
537
+ } else if (speakerSelection) {
538
+ payload.speaker_id = speakerSelection;
539
+ }
540
+
541
+ // Execute Request
542
+ audioBuffer = await httpRequest(url, 'POST', payload);
543
+ srtBuffer = Buffer.from(generateHeuristicSRT(text, audioBuffer.length), 'utf8');
544
+
380
545
  } else {
381
546
  // ----------------------------------
382
547
  // SYSTEM COMMAND EXECUTION
@@ -452,7 +617,7 @@ export class TTSBigBoss implements INodeType {
452
617
 
453
618
  returnData.push(newItem);
454
619
 
455
- } catch (error) {
620
+ } catch (error: any) {
456
621
  if (this.continueOnFail()) {
457
622
  returnData.push({ json: { error: error.message }, binary: {} });
458
623
  continue;
@@ -470,7 +635,13 @@ export class TTSBigBoss implements INodeType {
470
635
  // --------------------------------------------------------------------------
471
636
  async function runEdgeTTS(text: string, voice: string, rate: string, pitch: string): Promise<{ audio: Buffer; srt: string }> {
472
637
  return new Promise((resolve, reject) => {
473
- const ws = new WebSocket(EDGE_URL);
638
+ const ws = new WebSocket(EDGE_URL, {
639
+ headers: {
640
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0', // Updated UA to Edge
641
+ 'Origin': 'chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold', // Keep origin for now, usually required
642
+ 'TrustedClientToken': '6A5AA1D4EAFF4E9FB37E23D68491D6F4'
643
+ }
644
+ });
474
645
  const requestId = uuidv4().replace(/-/g, '');
475
646
  const audioChunks: Buffer[] = [];
476
647
  const wordBoundaries: WordBoundary[] = [];
@@ -755,13 +926,16 @@ async function ensurePiperModel(binDir: string, modelNameOrUrl: string): Promise
755
926
  await downloadFile(configUrl, configPath);
756
927
 
757
928
  // Validate JSON
929
+ if (!fs.existsSync(configPath)) {
930
+ throw new Error(`Failed to download config file: ${configPath}`);
931
+ }
758
932
  try {
759
933
  const content = fs.readFileSync(configPath, 'utf8');
760
934
  JSON.parse(content);
761
935
  } catch (e) {
762
936
  fs.unlinkSync(configPath); // Delete bad file
763
937
  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)}...`);
938
+ 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
939
  }
766
940
  }
767
941
 
@@ -771,20 +945,67 @@ async function ensurePiperModel(binDir: string, modelNameOrUrl: string): Promise
771
945
  async function downloadFile(url: string, dest: string): Promise<void> {
772
946
  return new Promise((resolve, reject) => {
773
947
  const file = fs.createWriteStream(dest);
774
- https.get(url, (response) => {
948
+
949
+ // Handle file system errors (e.g. permissions)
950
+ file.on('error', (err) => {
951
+ fs.unlink(dest, () => { }); // Cleanup
952
+ reject(new Error(`File write error: ${err.message}`));
953
+ });
954
+
955
+ const request = https.get(url, (response) => {
775
956
  if (response.statusCode === 302 || response.statusCode === 301) {
776
957
  // Follow redirect
958
+ file.close();
777
959
  downloadFile(response.headers.location!, dest).then(resolve).catch(reject);
778
960
  return;
779
961
  }
962
+
963
+ if (response.statusCode && response.statusCode !== 200) {
964
+ file.close();
965
+ fs.unlink(dest, () => { });
966
+ reject(new Error(`Download failed with status code: ${response.statusCode} for URL: ${url}`));
967
+ return;
968
+ }
969
+
780
970
  response.pipe(file);
971
+
781
972
  file.on('finish', () => {
782
- file.close();
783
- resolve();
973
+ file.close((err) => {
974
+ if (err) reject(err);
975
+ else resolve();
976
+ });
784
977
  });
785
- }).on('error', (err) => {
978
+ });
979
+
980
+ request.on('error', (err) => {
981
+ file.close();
786
982
  fs.unlink(dest, () => { });
787
- reject(err);
983
+ reject(new Error(`Network error: ${err.message}`));
984
+ });
985
+ });
986
+ }
987
+
988
+ async function httpRequest(url: string, method: string = 'GET', body: any = null): Promise<Buffer> {
989
+ const requestModule = url.startsWith('https') ? https : http;
990
+ return new Promise((resolve, reject) => {
991
+ const req = requestModule.request(url, {
992
+ method: method,
993
+ headers: {
994
+ 'Content-Type': 'application/json',
995
+ }
996
+ }, (res: any) => {
997
+ const chunks: any[] = [];
998
+ res.on('data', (d: any) => chunks.push(d));
999
+ res.on('end', () => {
1000
+ if (res.statusCode >= 200 && res.statusCode < 300) {
1001
+ resolve(Buffer.concat(chunks));
1002
+ } else {
1003
+ reject(new Error(`Server Request Failed ${res.statusCode}: ${Buffer.concat(chunks).toString()}`));
1004
+ }
1005
+ });
788
1006
  });
1007
+ req.on('error', reject);
1008
+ if (body) req.write(JSON.stringify(body));
1009
+ req.end();
789
1010
  });
790
1011
  }
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.7",
4
4
  "description": "BigBoss TTS node with multi-engine support and automatic SRT generation",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",