@ferchy/n8n-nodes-aimc-toolkit 0.1.4 → 0.1.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.
- package/README.md +56 -1
- package/dist/nodes/AimcDocling/AimcDocling.node.d.ts +5 -0
- package/dist/nodes/AimcDocling/AimcDocling.node.js +270 -0
- package/dist/nodes/AimcDocling/aimc-docling.svg +20 -0
- package/dist/nodes/AimcTts/AimcTts.node.d.ts +5 -0
- package/dist/nodes/AimcTts/AimcTts.node.js +276 -0
- package/dist/nodes/AimcTts/aimc-tts.svg +19 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# AIMC Toolkit for n8n
|
|
2
2
|
|
|
3
|
-
AIMC Toolkit is a community node package for n8n with
|
|
3
|
+
AIMC Toolkit is a community node package for n8n with focused nodes:
|
|
4
4
|
|
|
5
5
|
- **AIMC Code**: run JavaScript with a practical toolbox of libraries.
|
|
6
6
|
- **AIMC Media**: FFmpeg-powered media operations without extra glue nodes.
|
|
7
|
+
- **AIMC TTS**: CPU-friendly text-to-speech using Piper.
|
|
8
|
+
- **AIMC Docling**: OCR/document extraction using Docling.
|
|
7
9
|
|
|
8
10
|
## Why I Built This
|
|
9
11
|
|
|
@@ -21,6 +23,8 @@ n8n is powerful, but real workflows often need basic utilities (validation, pars
|
|
|
21
23
|
- **Fewer nodes, cleaner flows**: consolidate multiple steps into one code node.
|
|
22
24
|
- **Media ready**: convert, compress, merge, and inspect media in one place.
|
|
23
25
|
- **Practical libraries**: parsing, validation, dates, and web utilities built in.
|
|
26
|
+
- **Local voice**: generate speech on a CPU server without paid APIs.
|
|
27
|
+
- **Document extraction**: pull clean text/markdown from PDFs and scans.
|
|
24
28
|
|
|
25
29
|
## Installation
|
|
26
30
|
|
|
@@ -145,6 +149,57 @@ Audio File Path: /path/to/audio.mp3
|
|
|
145
149
|
**Large Files**
|
|
146
150
|
Use **Input Mode = File Path** to avoid loading big files into memory.
|
|
147
151
|
|
|
152
|
+
### AIMC TTS (Piper)
|
|
153
|
+
|
|
154
|
+
**What it does**
|
|
155
|
+
Generate speech locally using Piper (CPU-friendly, no external API). Output is WAV audio.
|
|
156
|
+
|
|
157
|
+
**Requirements**
|
|
158
|
+
- Python 3 installed
|
|
159
|
+
- Piper installed: `pip install piper-tts`
|
|
160
|
+
- A downloaded voice model
|
|
161
|
+
|
|
162
|
+
**Download a voice**
|
|
163
|
+
```bash
|
|
164
|
+
python3 -m piper.download_voices en_US-lessac-medium
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Example**
|
|
168
|
+
```
|
|
169
|
+
Text: Hello from AIMC Toolkit
|
|
170
|
+
Voice Name: en_US-lessac-medium
|
|
171
|
+
Output Mode: Binary
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Tips**
|
|
175
|
+
- Use **Output Mode = File Path** for large audio.
|
|
176
|
+
- For custom voice storage, set **Data Dir**.
|
|
177
|
+
|
|
178
|
+
### AIMC Docling (OCR + Document Parsing)
|
|
179
|
+
|
|
180
|
+
**What it does**
|
|
181
|
+
Extracts clean text/markdown from PDFs, images, and common document formats using Docling.
|
|
182
|
+
|
|
183
|
+
**Requirements**
|
|
184
|
+
- Python 3 installed
|
|
185
|
+
- Docling installed: `pip install docling`
|
|
186
|
+
|
|
187
|
+
**Input modes**
|
|
188
|
+
- Binary (from n8n)
|
|
189
|
+
- File Path (local file)
|
|
190
|
+
- URL (public file URL)
|
|
191
|
+
|
|
192
|
+
**Output formats**
|
|
193
|
+
- Markdown (default)
|
|
194
|
+
- JSON (structured output when available)
|
|
195
|
+
- Plain Text (markdown stripped)
|
|
196
|
+
|
|
197
|
+
**Example**
|
|
198
|
+
```
|
|
199
|
+
Input Mode: Binary
|
|
200
|
+
Output Format: Markdown
|
|
201
|
+
```
|
|
202
|
+
|
|
148
203
|
## Library Reference (AIMC Code)
|
|
149
204
|
|
|
150
205
|
Libraries are available as globals or via `libs.<name>`.
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.AimcDocling = void 0;
|
|
37
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const os = __importStar(require("os"));
|
|
41
|
+
const child_process_1 = require("child_process");
|
|
42
|
+
const util_1 = require("util");
|
|
43
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
44
|
+
let doclingChecked = null;
|
|
45
|
+
async function ensureDoclingAvailable(pythonPath) {
|
|
46
|
+
if (doclingChecked) {
|
|
47
|
+
if (!doclingChecked.ok) {
|
|
48
|
+
throw new Error(doclingChecked.message || 'Docling not available');
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
await execFileAsync(pythonPath, ['-c', 'import docling'], {
|
|
54
|
+
timeout: 10000,
|
|
55
|
+
maxBuffer: 1024 * 1024,
|
|
56
|
+
});
|
|
57
|
+
doclingChecked = { ok: true };
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const message = error instanceof Error && error.message
|
|
61
|
+
? error.message
|
|
62
|
+
: 'Docling not available';
|
|
63
|
+
doclingChecked = { ok: false, message };
|
|
64
|
+
throw new Error(message);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function createTempDir() {
|
|
68
|
+
return fs.promises.mkdtemp(path.join(os.tmpdir(), 'aimc-docling-'));
|
|
69
|
+
}
|
|
70
|
+
function safeFileName(name) {
|
|
71
|
+
return name.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
72
|
+
}
|
|
73
|
+
class AimcDocling {
|
|
74
|
+
constructor() {
|
|
75
|
+
this.description = {
|
|
76
|
+
displayName: 'AIMC Docling',
|
|
77
|
+
name: 'aimcDocling',
|
|
78
|
+
icon: 'file:aimc-docling.svg',
|
|
79
|
+
group: ['transform'],
|
|
80
|
+
version: 1,
|
|
81
|
+
description: 'Document extraction using Docling (OCR, PDF, DOCX).',
|
|
82
|
+
defaults: {
|
|
83
|
+
name: 'AIMC Docling',
|
|
84
|
+
},
|
|
85
|
+
inputs: ['main'],
|
|
86
|
+
outputs: ['main'],
|
|
87
|
+
properties: [
|
|
88
|
+
{
|
|
89
|
+
displayName: 'Input Mode',
|
|
90
|
+
name: 'inputMode',
|
|
91
|
+
type: 'options',
|
|
92
|
+
options: [
|
|
93
|
+
{ name: 'Binary', value: 'binary' },
|
|
94
|
+
{ name: 'File Path', value: 'filePath' },
|
|
95
|
+
{ name: 'URL', value: 'url' },
|
|
96
|
+
],
|
|
97
|
+
default: 'binary',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
displayName: 'Binary Property',
|
|
101
|
+
name: 'binaryProperty',
|
|
102
|
+
type: 'string',
|
|
103
|
+
default: 'data',
|
|
104
|
+
displayOptions: {
|
|
105
|
+
show: {
|
|
106
|
+
inputMode: ['binary'],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
displayName: 'File Path',
|
|
112
|
+
name: 'inputFilePath',
|
|
113
|
+
type: 'string',
|
|
114
|
+
default: '',
|
|
115
|
+
placeholder: '/path/to/document.pdf',
|
|
116
|
+
displayOptions: {
|
|
117
|
+
show: {
|
|
118
|
+
inputMode: ['filePath'],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
displayName: 'URL',
|
|
124
|
+
name: 'inputUrl',
|
|
125
|
+
type: 'string',
|
|
126
|
+
default: '',
|
|
127
|
+
placeholder: 'https://example.com/file.pdf',
|
|
128
|
+
displayOptions: {
|
|
129
|
+
show: {
|
|
130
|
+
inputMode: ['url'],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
displayName: 'Output Format',
|
|
136
|
+
name: 'outputFormat',
|
|
137
|
+
type: 'options',
|
|
138
|
+
options: [
|
|
139
|
+
{ name: 'Markdown', value: 'markdown' },
|
|
140
|
+
{ name: 'JSON', value: 'json' },
|
|
141
|
+
{ name: 'Plain Text', value: 'text' },
|
|
142
|
+
],
|
|
143
|
+
default: 'markdown',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
displayName: 'Python Path',
|
|
147
|
+
name: 'pythonPath',
|
|
148
|
+
type: 'string',
|
|
149
|
+
default: 'python3',
|
|
150
|
+
description: 'Path to Python binary with docling installed.',
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
displayName: 'Timeout (Seconds)',
|
|
154
|
+
name: 'timeoutSeconds',
|
|
155
|
+
type: 'number',
|
|
156
|
+
default: 120,
|
|
157
|
+
typeOptions: {
|
|
158
|
+
minValue: 30,
|
|
159
|
+
maxValue: 1800,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
async execute() {
|
|
166
|
+
const items = this.getInputData();
|
|
167
|
+
const results = [];
|
|
168
|
+
for (let index = 0; index < items.length; index++) {
|
|
169
|
+
const item = items[index];
|
|
170
|
+
const inputMode = this.getNodeParameter('inputMode', index);
|
|
171
|
+
const binaryProperty = this.getNodeParameter('binaryProperty', index, 'data');
|
|
172
|
+
const inputFilePath = this.getNodeParameter('inputFilePath', index, '');
|
|
173
|
+
const inputUrl = this.getNodeParameter('inputUrl', index, '');
|
|
174
|
+
const outputFormat = this.getNodeParameter('outputFormat', index, 'markdown');
|
|
175
|
+
const pythonPath = this.getNodeParameter('pythonPath', index, 'python3');
|
|
176
|
+
const timeoutSeconds = this.getNodeParameter('timeoutSeconds', index, 120);
|
|
177
|
+
let tempDir = null;
|
|
178
|
+
let source = '';
|
|
179
|
+
if (inputMode === 'binary') {
|
|
180
|
+
const binary = this.helpers.assertBinaryData(index, binaryProperty);
|
|
181
|
+
const buffer = await this.helpers.getBinaryDataBuffer(index, binaryProperty);
|
|
182
|
+
tempDir = await createTempDir();
|
|
183
|
+
const name = safeFileName(binary.fileName || `document.${binary.fileExtension || 'bin'}`);
|
|
184
|
+
const filePath = path.join(tempDir, name);
|
|
185
|
+
await fs.promises.writeFile(filePath, buffer);
|
|
186
|
+
source = filePath;
|
|
187
|
+
}
|
|
188
|
+
else if (inputMode === 'filePath') {
|
|
189
|
+
if (!inputFilePath) {
|
|
190
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'File Path is required.');
|
|
191
|
+
}
|
|
192
|
+
source = inputFilePath;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
if (!inputUrl) {
|
|
196
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'URL is required.');
|
|
197
|
+
}
|
|
198
|
+
source = inputUrl;
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
await ensureDoclingAvailable(pythonPath);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
const message = error instanceof Error ? error.message : 'Docling not available';
|
|
205
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Docling not found. Install with: pip install docling. ${message}`);
|
|
206
|
+
}
|
|
207
|
+
const script = `
|
|
208
|
+
+import json
|
|
209
|
+
+import sys
|
|
210
|
+
+from docling.document_converter import DocumentConverter
|
|
211
|
+
+
|
|
212
|
+
+source = ${JSON.stringify(source)}
|
|
213
|
+
+fmt = ${JSON.stringify(outputFormat)}
|
|
214
|
+
+
|
|
215
|
+
+converter = DocumentConverter()
|
|
216
|
+
+result = converter.convert(source)
|
|
217
|
+
+doc = result.document
|
|
218
|
+
+
|
|
219
|
+
+if fmt == 'markdown':
|
|
220
|
+
+ content = doc.export_to_markdown()
|
|
221
|
+
+elif fmt == 'text':
|
|
222
|
+
+ content = doc.export_to_markdown()
|
|
223
|
+
+ # naive text conversion
|
|
224
|
+
+ content = content.replace('#', '').replace('*', '')
|
|
225
|
+
+else:
|
|
226
|
+
+ try:
|
|
227
|
+
+ content = doc.model_dump()
|
|
228
|
+
+ except Exception:
|
|
229
|
+
+ content = {'text': doc.export_to_markdown()}
|
|
230
|
+
+
|
|
231
|
+
+payload = {"format": fmt, "content": content}
|
|
232
|
+
+print("__AIMC_RESULT__" + json.dumps(payload, default=str))
|
|
233
|
+
+`;
|
|
234
|
+
try {
|
|
235
|
+
const { stdout, stderr } = await execFileAsync(pythonPath, ['-c', script], {
|
|
236
|
+
timeout: Math.max(30, timeoutSeconds) * 1000,
|
|
237
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
238
|
+
});
|
|
239
|
+
const combined = `${stdout}\n${stderr}`.trim();
|
|
240
|
+
const marker = '__AIMC_RESULT__';
|
|
241
|
+
const markerIndex = combined.lastIndexOf(marker);
|
|
242
|
+
if (markerIndex === -1) {
|
|
243
|
+
throw new Error('Docling did not return a result.');
|
|
244
|
+
}
|
|
245
|
+
const jsonPayload = combined.slice(markerIndex + marker.length).trim();
|
|
246
|
+
const payload = JSON.parse(jsonPayload);
|
|
247
|
+
results.push({
|
|
248
|
+
json: {
|
|
249
|
+
...item.json,
|
|
250
|
+
docling: {
|
|
251
|
+
format: payload.format,
|
|
252
|
+
content: payload.content,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
259
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Docling failed: ${message}`);
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
if (tempDir) {
|
|
263
|
+
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return [results];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
exports.AimcDocling = AimcDocling;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="aimc-docling-bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#0A1021"/>
|
|
5
|
+
<stop offset="100%" stop-color="#0B1B3A"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="aimc-docling-glow" x1="0" y1="0" x2="1" y2="1">
|
|
8
|
+
<stop offset="0%" stop-color="#7DD3FC"/>
|
|
9
|
+
<stop offset="100%" stop-color="#0EA5E9"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
</defs>
|
|
12
|
+
<rect x="6" y="6" width="52" height="52" rx="10" fill="url(#aimc-docling-bg)"/>
|
|
13
|
+
<path d="M22 16H38L44 22V48H22Z" fill="#0F172A" stroke="#1E3A8A" stroke-width="2"/>
|
|
14
|
+
<path d="M38 16V22H44" stroke="#1E3A8A" stroke-width="2" fill="none"/>
|
|
15
|
+
<rect x="24" y="26" width="16" height="3" rx="1.5" fill="url(#aimc-docling-glow)"/>
|
|
16
|
+
<rect x="24" y="32" width="12" height="3" rx="1.5" fill="url(#aimc-docling-glow)"/>
|
|
17
|
+
<rect x="24" y="38" width="18" height="3" rx="1.5" fill="url(#aimc-docling-glow)"/>
|
|
18
|
+
<circle cx="46" cy="44" r="6" fill="#0B1223" stroke="#38BDF8" stroke-width="2"/>
|
|
19
|
+
<path d="M46 40V44L49 46" stroke="#38BDF8" stroke-width="2" stroke-linecap="round"/>
|
|
20
|
+
</svg>
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.AimcTts = void 0;
|
|
37
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const os = __importStar(require("os"));
|
|
41
|
+
const child_process_1 = require("child_process");
|
|
42
|
+
const util_1 = require("util");
|
|
43
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
44
|
+
let piperChecked = null;
|
|
45
|
+
async function ensurePiperAvailable(pythonPath) {
|
|
46
|
+
if (piperChecked) {
|
|
47
|
+
if (!piperChecked.ok) {
|
|
48
|
+
throw new Error(piperChecked.message || 'Piper not available');
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
await execFileAsync(pythonPath, ['-m', 'piper', '--help'], {
|
|
54
|
+
timeout: 10000,
|
|
55
|
+
maxBuffer: 1024 * 1024,
|
|
56
|
+
});
|
|
57
|
+
piperChecked = { ok: true };
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const message = error instanceof Error && error.message
|
|
61
|
+
? error.message
|
|
62
|
+
: 'Piper not available';
|
|
63
|
+
piperChecked = { ok: false, message };
|
|
64
|
+
throw new Error(message);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function createTempDir() {
|
|
68
|
+
return fs.promises.mkdtemp(path.join(os.tmpdir(), 'aimc-tts-'));
|
|
69
|
+
}
|
|
70
|
+
class AimcTts {
|
|
71
|
+
constructor() {
|
|
72
|
+
this.description = {
|
|
73
|
+
displayName: 'AIMC TTS',
|
|
74
|
+
name: 'aimcTts',
|
|
75
|
+
icon: 'file:aimc-tts.svg',
|
|
76
|
+
group: ['transform'],
|
|
77
|
+
version: 1,
|
|
78
|
+
description: 'Text-to-speech using Piper (CPU-friendly).',
|
|
79
|
+
defaults: {
|
|
80
|
+
name: 'AIMC TTS',
|
|
81
|
+
},
|
|
82
|
+
inputs: ['main'],
|
|
83
|
+
outputs: ['main'],
|
|
84
|
+
properties: [
|
|
85
|
+
{
|
|
86
|
+
displayName: 'Text',
|
|
87
|
+
name: 'text',
|
|
88
|
+
type: 'string',
|
|
89
|
+
default: '',
|
|
90
|
+
typeOptions: {
|
|
91
|
+
rows: 4,
|
|
92
|
+
},
|
|
93
|
+
description: 'Text to synthesize. Supports expressions from input data.',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
displayName: 'Voice Name',
|
|
97
|
+
name: 'voice',
|
|
98
|
+
type: 'string',
|
|
99
|
+
default: 'en_US-lessac-medium',
|
|
100
|
+
description: 'Piper voice name (downloaded via piper).',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
displayName: 'Data Dir',
|
|
104
|
+
name: 'dataDir',
|
|
105
|
+
type: 'string',
|
|
106
|
+
default: '',
|
|
107
|
+
description: 'Optional custom voice data directory.',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
displayName: 'Python Path',
|
|
111
|
+
name: 'pythonPath',
|
|
112
|
+
type: 'string',
|
|
113
|
+
default: 'python3',
|
|
114
|
+
description: 'Path to Python binary with piper-tts installed.',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
displayName: 'Sentence Silence (seconds)',
|
|
118
|
+
name: 'sentenceSilence',
|
|
119
|
+
type: 'number',
|
|
120
|
+
default: 0,
|
|
121
|
+
typeOptions: {
|
|
122
|
+
minValue: 0,
|
|
123
|
+
maxValue: 10,
|
|
124
|
+
},
|
|
125
|
+
description: 'Silence added after each sentence except the last.',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
displayName: 'Volume',
|
|
129
|
+
name: 'volume',
|
|
130
|
+
type: 'number',
|
|
131
|
+
default: 1,
|
|
132
|
+
typeOptions: {
|
|
133
|
+
minValue: 0.1,
|
|
134
|
+
maxValue: 3,
|
|
135
|
+
},
|
|
136
|
+
description: 'Volume multiplier (1.0 = default).',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
displayName: 'Disable Normalization',
|
|
140
|
+
name: 'noNormalize',
|
|
141
|
+
type: 'boolean',
|
|
142
|
+
default: false,
|
|
143
|
+
description: 'Disable automatic volume normalization.',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
displayName: 'Timeout (Seconds)',
|
|
147
|
+
name: 'timeoutSeconds',
|
|
148
|
+
type: 'number',
|
|
149
|
+
default: 60,
|
|
150
|
+
typeOptions: {
|
|
151
|
+
minValue: 10,
|
|
152
|
+
maxValue: 600,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
displayName: 'Output Mode',
|
|
157
|
+
name: 'outputMode',
|
|
158
|
+
type: 'options',
|
|
159
|
+
options: [
|
|
160
|
+
{ name: 'Binary', value: 'binary' },
|
|
161
|
+
{ name: 'File Path', value: 'filePath' },
|
|
162
|
+
],
|
|
163
|
+
default: 'binary',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
displayName: 'Output Binary Property',
|
|
167
|
+
name: 'outputBinaryProperty',
|
|
168
|
+
type: 'string',
|
|
169
|
+
default: 'data',
|
|
170
|
+
displayOptions: {
|
|
171
|
+
show: {
|
|
172
|
+
outputMode: ['binary'],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
displayName: 'Output File Path',
|
|
178
|
+
name: 'outputFilePath',
|
|
179
|
+
type: 'string',
|
|
180
|
+
default: '',
|
|
181
|
+
placeholder: '/path/to/output.wav',
|
|
182
|
+
displayOptions: {
|
|
183
|
+
show: {
|
|
184
|
+
outputMode: ['filePath'],
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async execute() {
|
|
192
|
+
const items = this.getInputData();
|
|
193
|
+
const results = [];
|
|
194
|
+
for (let index = 0; index < items.length; index++) {
|
|
195
|
+
const item = items[index];
|
|
196
|
+
const text = this.getNodeParameter('text', index, '');
|
|
197
|
+
const voice = this.getNodeParameter('voice', index);
|
|
198
|
+
const dataDir = this.getNodeParameter('dataDir', index, '');
|
|
199
|
+
const pythonPath = this.getNodeParameter('pythonPath', index, 'python3');
|
|
200
|
+
const sentenceSilence = this.getNodeParameter('sentenceSilence', index, 0);
|
|
201
|
+
const volume = this.getNodeParameter('volume', index, 1);
|
|
202
|
+
const noNormalize = this.getNodeParameter('noNormalize', index, false);
|
|
203
|
+
const timeoutSeconds = this.getNodeParameter('timeoutSeconds', index, 60);
|
|
204
|
+
const outputMode = this.getNodeParameter('outputMode', index, 'binary');
|
|
205
|
+
const outputBinaryProperty = this.getNodeParameter('outputBinaryProperty', index, 'data');
|
|
206
|
+
const outputFilePath = this.getNodeParameter('outputFilePath', index, '');
|
|
207
|
+
if (!text || !text.trim()) {
|
|
208
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Text is required.');
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
await ensurePiperAvailable(pythonPath);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
const message = error instanceof Error ? error.message : 'Piper not available';
|
|
215
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Piper not found. Install piper-tts and voices. ${message}`);
|
|
216
|
+
}
|
|
217
|
+
let tempDir = null;
|
|
218
|
+
let outputPath = outputFilePath;
|
|
219
|
+
if (outputMode === 'filePath' && !outputPath) {
|
|
220
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Output File Path is required.');
|
|
221
|
+
}
|
|
222
|
+
if (outputMode === 'binary') {
|
|
223
|
+
tempDir = await createTempDir();
|
|
224
|
+
outputPath = path.join(tempDir, 'speech.wav');
|
|
225
|
+
}
|
|
226
|
+
const args = ['-m', 'piper', '-m', voice, '-f', outputPath];
|
|
227
|
+
if (dataDir) {
|
|
228
|
+
args.push('--data-dir', dataDir);
|
|
229
|
+
}
|
|
230
|
+
if (sentenceSilence > 0) {
|
|
231
|
+
args.push('--sentence-silence', sentenceSilence.toString());
|
|
232
|
+
}
|
|
233
|
+
if (volume && volume !== 1) {
|
|
234
|
+
args.push('--volume', volume.toString());
|
|
235
|
+
}
|
|
236
|
+
if (noNormalize) {
|
|
237
|
+
args.push('--no-normalize');
|
|
238
|
+
}
|
|
239
|
+
args.push('--', text);
|
|
240
|
+
try {
|
|
241
|
+
await execFileAsync(pythonPath, args, {
|
|
242
|
+
timeout: Math.max(10, timeoutSeconds) * 1000,
|
|
243
|
+
maxBuffer: 1024 * 1024,
|
|
244
|
+
});
|
|
245
|
+
const outputItem = {
|
|
246
|
+
json: {
|
|
247
|
+
...item.json,
|
|
248
|
+
tts: {
|
|
249
|
+
voice,
|
|
250
|
+
outputPath: outputMode === 'filePath' ? outputPath : undefined,
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
if (outputMode === 'binary') {
|
|
255
|
+
const data = await fs.promises.readFile(outputPath);
|
|
256
|
+
const binaryData = await this.helpers.prepareBinaryData(data, 'speech.wav');
|
|
257
|
+
outputItem.binary = {
|
|
258
|
+
[outputBinaryProperty]: binaryData,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
results.push(outputItem);
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
265
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `TTS failed: ${message}`);
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
if (tempDir) {
|
|
269
|
+
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return [results];
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
exports.AimcTts = AimcTts;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="aimc-tts-bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#0A1021"/>
|
|
5
|
+
<stop offset="100%" stop-color="#0B1B3A"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="aimc-tts-glow" x1="0" y1="0" x2="1" y2="1">
|
|
8
|
+
<stop offset="0%" stop-color="#7DD3FC"/>
|
|
9
|
+
<stop offset="100%" stop-color="#0EA5E9"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
</defs>
|
|
12
|
+
<rect x="6" y="6" width="52" height="52" rx="10" fill="url(#aimc-tts-bg)"/>
|
|
13
|
+
<rect x="26" y="18" width="12" height="22" rx="6" fill="url(#aimc-tts-glow)"/>
|
|
14
|
+
<path d="M22 30C22 36 26 40 32 40C38 40 42 36 42 30" stroke="#38BDF8" stroke-width="2.5" fill="none" stroke-linecap="round"/>
|
|
15
|
+
<path d="M24 46H40" stroke="#1E3A8A" stroke-width="2" stroke-linecap="round"/>
|
|
16
|
+
<path d="M32 40V46" stroke="#1E3A8A" stroke-width="2" stroke-linecap="round"/>
|
|
17
|
+
<path d="M14 24C18 20 20 20 22 24" stroke="#1E3A8A" stroke-width="1.5" fill="none" stroke-linecap="round"/>
|
|
18
|
+
<path d="M50 24C46 20 44 20 42 24" stroke="#1E3A8A" stroke-width="1.5" fill="none" stroke-linecap="round"/>
|
|
19
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ferchy/n8n-nodes-aimc-toolkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "AIMC Toolkit nodes for n8n: code execution and media operations.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ferchy",
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
"n8nNodesApiVersion": 1,
|
|
29
29
|
"nodes": [
|
|
30
30
|
"dist/nodes/AimcCode/AimcCode.node.js",
|
|
31
|
-
"dist/nodes/AimcMedia/AimcMedia.node.js"
|
|
31
|
+
"dist/nodes/AimcMedia/AimcMedia.node.js",
|
|
32
|
+
"dist/nodes/AimcTts/AimcTts.node.js",
|
|
33
|
+
"dist/nodes/AimcDocling/AimcDocling.node.js"
|
|
32
34
|
]
|
|
33
35
|
},
|
|
34
36
|
"dependencies": {
|