n8n-nodes-tts-bigboss 1.0.2 → 1.0.3

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.
@@ -47,6 +47,30 @@ const https = __importStar(require("https"));
47
47
  const stream = __importStar(require("stream"));
48
48
  const util_1 = require("util");
49
49
  const pipeline = (0, util_1.promisify)(stream.pipeline);
50
+ 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' },
73
+ ];
50
74
  const EDGE_URL = 'wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1?TrustedClientToken=6A5AA1D4EAFF4E9FB37E23D68491D6F4';
51
75
  const EDGE_VOICES = [
52
76
  { name: 'Arabic (Egypt) - Salma', value: 'ar-EG-SalmaNeural' },
@@ -217,17 +241,35 @@ class TTSBigBoss {
217
241
  description: 'Binary property name containing the reference audio for cloning. Use placeholder "{reference_audio}" in command.',
218
242
  },
219
243
  {
220
- displayName: 'Piper Voice Model (HF URL or Path)',
244
+ displayName: 'Piper Voice Model',
221
245
  name: 'piperModel',
222
- type: 'string',
246
+ type: 'options',
247
+ options: [
248
+ ...PIPER_MODELS,
249
+ { name: 'Custom (Enter URL)', value: 'custom' },
250
+ ],
223
251
  default: 'en_US-lessac-medium',
224
- description: 'Enter a known Piper model name (e.g. "en_US-lessac-medium") which will be auto-downloaded, OR a full URL to the .onnx file.',
225
252
  displayOptions: {
226
253
  show: {
227
254
  engine: ['piper_local'],
228
255
  },
229
256
  },
230
- }
257
+ description: 'Select a voice model. It will be downloaded automatically on first use.',
258
+ },
259
+ {
260
+ displayName: 'Custom Model Name/URL',
261
+ name: 'piperModelCustom',
262
+ type: 'string',
263
+ default: '',
264
+ placeholder: 'e.g. en_US-bryce-medium',
265
+ displayOptions: {
266
+ show: {
267
+ engine: ['piper_local'],
268
+ piperModel: ['custom'],
269
+ },
270
+ },
271
+ description: 'Name from Hugging Face (e.g. en_US-bryce-medium) or full URL to .onnx file.',
272
+ },
231
273
  ],
232
274
  };
233
275
  }
@@ -264,7 +306,10 @@ class TTSBigBoss {
264
306
  }
265
307
  }
266
308
  else if (engine === 'piper_local') {
267
- const piperModel = this.getNodeParameter('piperModel', i);
309
+ let piperModel = this.getNodeParameter('piperModel', i);
310
+ if (piperModel === 'custom') {
311
+ piperModel = this.getNodeParameter('piperModelCustom', i);
312
+ }
268
313
  const piperBinPath = await ensurePiperBinary(binDir);
269
314
  const { modelPath, configPath } = await ensurePiperModel(binDir, piperModel);
270
315
  const outFile = path.join(tempDir, `piper_out_${(0, uuid_1.v4)()}.wav`);
@@ -281,8 +326,12 @@ class TTSBigBoss {
281
326
  piperProc.on('close', (code) => {
282
327
  if (code === 0)
283
328
  resolve();
284
- else
329
+ if (errData.includes('json.exception.parse_error')) {
330
+ reject(new Error(`Piper Config Error: The downloaded JSON configuration for model '${piperModel}' seems corrupted (HTML instead of JSON?). Try deleting the file at ${configPath} and running again.`));
331
+ }
332
+ else {
285
333
  reject(new Error(`Piper failed (exit ${code}): ${errData}`));
334
+ }
286
335
  });
287
336
  piperProc.on('error', (err) => reject(err));
288
337
  });
@@ -522,30 +571,48 @@ async function ensurePiperBinary(binDir) {
522
571
  }
523
572
  }
524
573
  async function ensurePiperModel(binDir, modelNameOrUrl) {
574
+ let modelUrl = '';
575
+ let modelFilename = '';
525
576
  if (modelNameOrUrl.startsWith('http')) {
526
- const fileName = path.basename(modelNameOrUrl);
527
- const modelPath = path.join(binDir, fileName);
528
- if (!fs.existsSync(modelPath)) {
529
- await downloadFile(modelNameOrUrl, modelPath);
577
+ modelUrl = modelNameOrUrl;
578
+ modelFilename = path.basename(modelNameOrUrl);
579
+ }
580
+ else {
581
+ const parts = modelNameOrUrl.split('-');
582
+ if (parts.length >= 3) {
583
+ const langRegion = parts[0] + '_' + parts[1];
584
+ const voice = parts[2];
585
+ const quality = parts[3] || 'medium';
586
+ const lang = parts[0];
587
+ modelFilename = modelNameOrUrl + '.onnx';
588
+ modelUrl = `https://huggingface.co/rhasspy/piper-voices/resolve/main/${lang}/${langRegion}/${voice}/${quality}/${modelFilename}?download=true`;
530
589
  }
531
- const configUrl = modelNameOrUrl + '.json';
532
- const configPath = modelPath + '.json';
533
- if (!fs.existsSync(configPath)) {
534
- await downloadFile(configUrl, configPath);
590
+ else {
591
+ throw new Error(`Invalid model name format: ${modelNameOrUrl}. Use format lang_REGION-voice-quality`);
535
592
  }
536
- return { modelPath, configPath };
537
593
  }
538
- if (modelNameOrUrl === 'en_US-lessac-medium') {
539
- const url = 'https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx';
540
- const modelPath = path.join(binDir, 'en_US-lessac-medium.onnx');
541
- const configPath = path.join(binDir, 'en_US-lessac-medium.onnx.json');
542
- if (!fs.existsSync(modelPath))
543
- await downloadFile(url, modelPath);
544
- if (!fs.existsSync(configPath))
545
- await downloadFile(url + '.json', configPath);
546
- return { modelPath, configPath };
594
+ const modelPath = path.join(binDir, modelFilename);
595
+ const configPath = modelPath + '.json';
596
+ if (!fs.existsSync(modelPath) || fs.statSync(modelPath).size < 1000) {
597
+ console.log(`Downloading Piper Model: ${modelUrl}`);
598
+ await downloadFile(modelUrl, modelPath);
599
+ }
600
+ if (!fs.existsSync(configPath) || fs.statSync(configPath).size < 10) {
601
+ const configUrl = modelUrl + '.json';
602
+ console.log(`Downloading Piper Config: ${configUrl}`);
603
+ await downloadFile(configUrl, configPath);
604
+ try {
605
+ const content = fs.readFileSync(configPath, 'utf8');
606
+ JSON.parse(content);
607
+ }
608
+ catch (e) {
609
+ fs.unlinkSync(configPath);
610
+ if (fs.existsSync(modelPath))
611
+ 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)}...`);
613
+ }
547
614
  }
548
- return { modelPath: modelNameOrUrl, configPath: modelNameOrUrl + '.json' };
615
+ return { modelPath, configPath };
549
616
  }
550
617
  async function downloadFile(url, dest) {
551
618
  return new Promise((resolve, reject) => {
@@ -17,6 +17,43 @@ import * as zlib from 'zlib'; // For extracting .tar.gz if needed, typically usa
17
17
 
18
18
  const pipeline = promisify(stream.pipeline);
19
19
 
20
+ // Piper Models List (Curated High Quality)
21
+ const PIPER_MODELS = [
22
+ // 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' },
25
+
26
+ // 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' },
36
+
37
+ // 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
+
42
+ // 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' },
45
+
46
+ // 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' },
49
+
50
+ // 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' },
55
+ ];
56
+
20
57
  // Edge TTS Constants
21
58
  const EDGE_URL = 'wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1?TrustedClientToken=6A5AA1D4EAFF4E9FB37E23D68491D6F4';
22
59
  const EDGE_VOICES = [
@@ -212,17 +249,35 @@ export class TTSBigBoss implements INodeType {
212
249
  // Local Piper Settings
213
250
  // ----------------------------------
214
251
  {
215
- displayName: 'Piper Voice Model (HF URL or Path)',
252
+ displayName: 'Piper Voice Model',
216
253
  name: 'piperModel',
217
- type: 'string',
254
+ type: 'options',
255
+ options: [
256
+ ...PIPER_MODELS,
257
+ { name: 'Custom (Enter URL)', value: 'custom' },
258
+ ],
218
259
  default: 'en_US-lessac-medium',
219
- description: 'Enter a known Piper model name (e.g. "en_US-lessac-medium") which will be auto-downloaded, OR a full URL to the .onnx file.',
220
260
  displayOptions: {
221
261
  show: {
222
262
  engine: ['piper_local'],
223
263
  },
224
264
  },
225
- }
265
+ description: 'Select a voice model. It will be downloaded automatically on first use.',
266
+ },
267
+ {
268
+ displayName: 'Custom Model Name/URL',
269
+ name: 'piperModelCustom',
270
+ type: 'string',
271
+ default: '',
272
+ placeholder: 'e.g. en_US-bryce-medium',
273
+ displayOptions: {
274
+ show: {
275
+ engine: ['piper_local'],
276
+ piperModel: ['custom'],
277
+ },
278
+ },
279
+ description: 'Name from Hugging Face (e.g. en_US-bryce-medium) or full URL to .onnx file.',
280
+ },
226
281
  ],
227
282
  };
228
283
 
@@ -271,7 +326,10 @@ export class TTSBigBoss implements INodeType {
271
326
  // ----------------------------------
272
327
  // PIPER LOCAL AUTOMATION
273
328
  // ----------------------------------
274
- const piperModel = this.getNodeParameter('piperModel', i) as string;
329
+ let piperModel = this.getNodeParameter('piperModel', i) as string;
330
+ if (piperModel === 'custom') {
331
+ piperModel = this.getNodeParameter('piperModelCustom', i) as string;
332
+ }
275
333
 
276
334
  // 1. Ensure Piper Binary
277
335
  const piperBinPath = await ensurePiperBinary(binDir);
@@ -299,7 +357,12 @@ export class TTSBigBoss implements INodeType {
299
357
 
300
358
  piperProc.on('close', (code) => {
301
359
  if (code === 0) resolve();
302
- else reject(new Error(`Piper failed (exit ${code}): ${errData}`));
360
+ // Check for the specific JSON error in stderr
361
+ if (errData.includes('json.exception.parse_error')) {
362
+ reject(new Error(`Piper Config Error: The downloaded JSON configuration for model '${piperModel}' seems corrupted (HTML instead of JSON?). Try deleting the file at ${configPath} and running again.`));
363
+ } else {
364
+ reject(new Error(`Piper failed (exit ${code}): ${errData}`));
365
+ }
303
366
  });
304
367
 
305
368
  piperProc.on('error', (err) => reject(err));
@@ -641,42 +704,66 @@ async function ensurePiperBinary(binDir: string): Promise<string> {
641
704
  }
642
705
 
643
706
  async function ensurePiperModel(binDir: string, modelNameOrUrl: string): Promise<{ modelPath: string, configPath: string }> {
644
- // If it's a known model name like "en_US-lessac-medium", construct URL
645
- // https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx
646
- // This is tricky because directory structure varies.
647
- // We'll rely on a direct URL or a simplied map for BigBoss defaults.
707
+ // Heuristic for HF URL construction
708
+ // https://huggingface.co/rhasspy/piper-voices/resolve/main/[lang_code]/[region]/[voice]/[quality]/[filename]
709
+ // Example: en_US-lessac-medium -> en/en_US/lessac/medium/en_US-lessac-medium.onnx
648
710
 
649
- // If full URL provided
650
- if (modelNameOrUrl.startsWith('http')) {
651
- const fileName = path.basename(modelNameOrUrl);
652
- const modelPath = path.join(binDir, fileName);
653
- if (!fs.existsSync(modelPath)) {
654
- await downloadFile(modelNameOrUrl, modelPath);
655
- }
711
+ let modelUrl = '';
712
+ let modelFilename = '';
656
713
 
657
- const configUrl = modelNameOrUrl + '.json';
658
- const configPath = modelPath + '.json';
659
- if (!fs.existsSync(configPath)) {
660
- await downloadFile(configUrl, configPath);
714
+ if (modelNameOrUrl.startsWith('http')) {
715
+ modelUrl = modelNameOrUrl;
716
+ modelFilename = path.basename(modelNameOrUrl);
717
+ } else {
718
+ // Construct URL from name
719
+ const parts = modelNameOrUrl.split('-');
720
+ if (parts.length >= 3) {
721
+ const langRegion = parts[0] + '_' + parts[1]; // en_US
722
+ const voice = parts[2];
723
+ const quality = parts[3] || 'medium';
724
+ const lang = parts[0]; // en
725
+
726
+ // e.g. en_US-lessac-medium
727
+ // lang=en, region=en_US, voice=lessac, quality=medium
728
+ // url path: en/en_US/lessac/medium/en_US-lessac-medium.onnx
729
+
730
+ // Handle special case: ar_JO (no lang folder? check repo)
731
+ // Generally structure is: lang_short/lang_long/voice/quality/filename
732
+
733
+ modelFilename = modelNameOrUrl + '.onnx';
734
+ modelUrl = `https://huggingface.co/rhasspy/piper-voices/resolve/main/${lang}/${langRegion}/${voice}/${quality}/${modelFilename}?download=true`; // Add download=true to force direct link
735
+ } else {
736
+ throw new Error(`Invalid model name format: ${modelNameOrUrl}. Use format lang_REGION-voice-quality`);
661
737
  }
662
- return { modelPath, configPath };
663
738
  }
664
739
 
665
- // If short name provided, try a heuristic for common English voice
666
- // We only support auto-download for "en_US-lessac-medium" easily for now.
667
- if (modelNameOrUrl === 'en_US-lessac-medium') {
668
- const url = 'https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx';
669
- const modelPath = path.join(binDir, 'en_US-lessac-medium.onnx');
670
- const configPath = path.join(binDir, 'en_US-lessac-medium.onnx.json');
740
+ const modelPath = path.join(binDir, modelFilename);
741
+ const configPath = modelPath + '.json';
671
742
 
672
- if (!fs.existsSync(modelPath)) await downloadFile(url, modelPath);
673
- if (!fs.existsSync(configPath)) await downloadFile(url + '.json', configPath);
743
+ // Download if missing or seems incomplete (size < 1KB)
744
+ if (!fs.existsSync(modelPath) || fs.statSync(modelPath).size < 1000) {
745
+ console.log(`Downloading Piper Model: ${modelUrl}`);
746
+ await downloadFile(modelUrl, modelPath);
747
+ }
674
748
 
675
- return { modelPath, configPath };
749
+ // Download config if missing or seems incomplete (size < 10 bytes)
750
+ if (!fs.existsSync(configPath) || fs.statSync(configPath).size < 10) {
751
+ const configUrl = modelUrl + '.json';
752
+ console.log(`Downloading Piper Config: ${configUrl}`);
753
+ await downloadFile(configUrl, configPath);
754
+
755
+ // Validate JSON
756
+ try {
757
+ const content = fs.readFileSync(configPath, 'utf8');
758
+ JSON.parse(content);
759
+ } catch (e) {
760
+ fs.unlinkSync(configPath); // Delete bad file
761
+ 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)}...`);
763
+ }
676
764
  }
677
765
 
678
- // Assume local path if not url/special name
679
- return { modelPath: modelNameOrUrl, configPath: modelNameOrUrl + '.json' };
766
+ return { modelPath, configPath };
680
767
  }
681
768
 
682
769
  async function downloadFile(url: string, dest: string): Promise<void> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-tts-bigboss",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "BigBoss TTS node with multi-engine support and automatic SRT generation",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",