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.
- package/dist/TTSBigBoss.node.js +208 -7
- package/nodes/TTSBigBoss/TTSBigBoss.node.ts +229 -8
- 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.',
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
796
|
+
file.close((err) => {
|
|
797
|
+
if (err)
|
|
798
|
+
reject(err);
|
|
799
|
+
else
|
|
800
|
+
resolve();
|
|
801
|
+
});
|
|
629
802
|
});
|
|
630
|
-
})
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
973
|
+
file.close((err) => {
|
|
974
|
+
if (err) reject(err);
|
|
975
|
+
else resolve();
|
|
976
|
+
});
|
|
784
977
|
});
|
|
785
|
-
})
|
|
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
|
}
|