@comandosai/n8n-nodes-media 0.1.2
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/Media.png +0 -0
- package/README.md +86 -0
- package/credentials/ComandosApi.credentials.ts +29 -0
- package/dist/credentials/ComandosApi.credentials.d.ts +7 -0
- package/dist/credentials/ComandosApi.credentials.js +33 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/nodes/ComandosImage.node.d.ts +9 -0
- package/dist/nodes/ComandosImage.node.js +310 -0
- package/dist/nodes/ComandosMedia.node.d.ts +5 -0
- package/dist/nodes/ComandosMedia.node.js +310 -0
- package/dist/nodes/Image.png +0 -0
- package/dist/nodes/Media.png +0 -0
- package/image-payload.json +3 -0
- package/index.ts +4 -0
- package/nodes/ComandosMedia.node.ts +334 -0
- package/package.json +25 -0
- package/tsconfig.json +15 -0
package/Media.png
ADDED
|
Binary file
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Comandos Media (кастомная нода n8n)
|
|
2
|
+
|
|
3
|
+
Кастомная нода для создания задач генерации изображений и видео через Commandos API.
|
|
4
|
+
|
|
5
|
+
## Где находится
|
|
6
|
+
|
|
7
|
+
- Код ноды: `/root/sandbox/nodes/image`
|
|
8
|
+
- Пакет: `@comandosai/n8n-nodes-media`
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## Требования
|
|
12
|
+
|
|
13
|
+
- n8n (dev окружение)
|
|
14
|
+
- Node.js 18+
|
|
15
|
+
- Доступ к Commandos API
|
|
16
|
+
|
|
17
|
+
## Подключение в n8n-dev
|
|
18
|
+
|
|
19
|
+
В `docker-compose` уже настроено:
|
|
20
|
+
|
|
21
|
+
- монтирование `/root/sandbox/nodes` в `/custom/commandos`
|
|
22
|
+
- переменная `N8N_CUSTOM_EXTENSIONS=/custom/commandos`
|
|
23
|
+
|
|
24
|
+
Если переносите в другое окружение, убедитесь, что путь задан корректно и n8n перезапущен.
|
|
25
|
+
|
|
26
|
+
## Credentials
|
|
27
|
+
|
|
28
|
+
Тип: `Commandos API`
|
|
29
|
+
|
|
30
|
+
Поля:
|
|
31
|
+
- `License Key` — передаётся как `X-License-Key`.
|
|
32
|
+
|
|
33
|
+
## Операции
|
|
34
|
+
|
|
35
|
+
### Create Task
|
|
36
|
+
|
|
37
|
+
Создаёт задачу в API (`POST /tasks`, `process_type=IMAGE_GENERATION`). Нода сама формирует:
|
|
38
|
+
|
|
39
|
+
- `url` для генерации
|
|
40
|
+
- `body` для выбранной модели
|
|
41
|
+
|
|
42
|
+
Входные поля:
|
|
43
|
+
- `Model`: Flux Pro, GPT-4o Image, Nano Banana, Nano Banana Pro, Seedream, Midjourney
|
|
44
|
+
- `Prompt`
|
|
45
|
+
- `Ratio`: `1:1`, `2:3`, `3:2`, `4:5`, `16:9`, `9:16`
|
|
46
|
+
- `References`: до 2 URL (опционально)
|
|
47
|
+
|
|
48
|
+
Выход:
|
|
49
|
+
- `taskId`, `status`, `pollUrl`
|
|
50
|
+
|
|
51
|
+
### Check Status
|
|
52
|
+
|
|
53
|
+
Запрашивает статус по `taskId` (`GET /tasks/:taskId`).
|
|
54
|
+
|
|
55
|
+
## Базовый URL API
|
|
56
|
+
|
|
57
|
+
Нода всегда использует `https://api.comandos.ai`.
|
|
58
|
+
|
|
59
|
+
## Переменные окружения (URL генерации)
|
|
60
|
+
|
|
61
|
+
Нода берёт URL генерации из переменных окружения:
|
|
62
|
+
|
|
63
|
+
- `COMMANDOS_IMAGE_URL_DEFAULT`
|
|
64
|
+
- `COMMANDOS_IMAGE_URL_GPT4O` (опционально)
|
|
65
|
+
- `COMMANDOS_IMAGE_URL_MJ` (опционально)
|
|
66
|
+
|
|
67
|
+
Если специальные URL не заданы, используется `COMMANDOS_IMAGE_URL_DEFAULT`.
|
|
68
|
+
|
|
69
|
+
## Пример workflow
|
|
70
|
+
|
|
71
|
+
1. **Commandos Image** → `Create Task`
|
|
72
|
+
2. **Wait** (20–60 секунд)
|
|
73
|
+
3. **Commandos Image** → `Check Status` (`{{$json.taskId}}`)
|
|
74
|
+
|
|
75
|
+
## Сборка
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install
|
|
79
|
+
npm run build
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
После изменений всегда пересобирайте `dist` и перезапускайте n8n.
|
|
83
|
+
|
|
84
|
+
## Иконка
|
|
85
|
+
|
|
86
|
+
Файл иконки: `Image.png` (копируется в `dist/nodes` при сборке).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
|
|
2
|
+
|
|
3
|
+
export class ComandosApi implements ICredentialType {
|
|
4
|
+
name = 'comandosApi';
|
|
5
|
+
displayName = 'Comandos API';
|
|
6
|
+
documentationUrl = 'https://api.comandos.ai';
|
|
7
|
+
properties: INodeProperties[] = [
|
|
8
|
+
{
|
|
9
|
+
displayName: 'License Key',
|
|
10
|
+
name: 'licenseKey',
|
|
11
|
+
type: 'string',
|
|
12
|
+
default: '',
|
|
13
|
+
required: true,
|
|
14
|
+
typeOptions: {
|
|
15
|
+
password: true,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
displayName: 'Kie.ai API Key',
|
|
20
|
+
name: 'kieApiKey',
|
|
21
|
+
type: 'string',
|
|
22
|
+
default: '',
|
|
23
|
+
required: false,
|
|
24
|
+
typeOptions: {
|
|
25
|
+
password: true,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ComandosApi = void 0;
|
|
4
|
+
class ComandosApi {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.name = 'comandosApi';
|
|
7
|
+
this.displayName = 'Comandos API';
|
|
8
|
+
this.documentationUrl = 'https://api.comandos.ai';
|
|
9
|
+
this.properties = [
|
|
10
|
+
{
|
|
11
|
+
displayName: 'License Key',
|
|
12
|
+
name: 'licenseKey',
|
|
13
|
+
type: 'string',
|
|
14
|
+
default: '',
|
|
15
|
+
required: true,
|
|
16
|
+
typeOptions: {
|
|
17
|
+
password: true,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
displayName: 'Kie.ai API Key',
|
|
22
|
+
name: 'kieApiKey',
|
|
23
|
+
type: 'string',
|
|
24
|
+
default: '',
|
|
25
|
+
required: false,
|
|
26
|
+
typeOptions: {
|
|
27
|
+
password: true,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.ComandosApi = ComandosApi;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ComandosApi = exports.ComandosMedia = void 0;
|
|
4
|
+
const ComandosMedia_node_1 = require("./nodes/ComandosMedia.node");
|
|
5
|
+
Object.defineProperty(exports, "ComandosMedia", { enumerable: true, get: function () { return ComandosMedia_node_1.ComandosMedia; } });
|
|
6
|
+
const ComandosApi_credentials_1 = require("./credentials/ComandosApi.credentials");
|
|
7
|
+
Object.defineProperty(exports, "ComandosApi", { enumerable: true, get: function () { return ComandosApi_credentials_1.ComandosApi; } });
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
2
|
+
export declare class ComandosImage implements INodeType {
|
|
3
|
+
displayName: 'Comandos Media';
|
|
4
|
+
name: 'comandosMedia';
|
|
5
|
+
group: ['transform'];
|
|
6
|
+
version: 1;
|
|
7
|
+
description: INodeTypeDescription;
|
|
8
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ComandosImage = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
const COMANDOS_API_URL = String(process.env.COMANDOS_API_BASE_URL || process.env.COMMANDOS_API_BASE_URL || "https://api.comandos.ai").trim();
|
|
6
|
+
const GENERATION_URL_DEFAULT = String(process.env.COMANDOS_IMAGE_URL_DEFAULT || process.env.COMMANDOS_IMAGE_URL_DEFAULT || "").trim();
|
|
7
|
+
const GENERATION_URL_MJ = String(process.env.COMANDOS_IMAGE_URL_MJ || process.env.COMMANDOS_IMAGE_URL_MJ || "").trim();
|
|
8
|
+
const resolveGenerationUrl = (resolvedModel) => {
|
|
9
|
+
if (resolvedModel === 'midjourney') {
|
|
10
|
+
return GENERATION_URL_MJ || GENERATION_URL_DEFAULT;
|
|
11
|
+
}
|
|
12
|
+
return GENERATION_URL_DEFAULT;
|
|
13
|
+
};
|
|
14
|
+
const mapRatio = (model, ratio) => {
|
|
15
|
+
const isSeedream = model.includes('seedream');
|
|
16
|
+
if (isSeedream) {
|
|
17
|
+
if (ratio === '2:3')
|
|
18
|
+
return 'portrait_3_2';
|
|
19
|
+
if (ratio === '3:2')
|
|
20
|
+
return 'landscape_2_3';
|
|
21
|
+
}
|
|
22
|
+
return ratio;
|
|
23
|
+
};
|
|
24
|
+
const buildGenerationRequest = ({ model, prompt, ratio, references, quality = 'basic', resolution = '1K', }) => {
|
|
25
|
+
const hasReferences = references.length > 0;
|
|
26
|
+
const requestedModel = model;
|
|
27
|
+
let resolvedModel = model;
|
|
28
|
+
if (requestedModel === 'flux-pro') {
|
|
29
|
+
resolvedModel = hasReferences ? 'flux-2/pro-image-to-image' : 'flux-2/pro-text-to-image';
|
|
30
|
+
}
|
|
31
|
+
else if (requestedModel === 'nano-banana') {
|
|
32
|
+
resolvedModel = hasReferences ? 'google/nano-banana-edit' : 'google/nano-banana';
|
|
33
|
+
}
|
|
34
|
+
else if (requestedModel === 'nano-banana-pro') {
|
|
35
|
+
resolvedModel = 'nano-banana-pro';
|
|
36
|
+
}
|
|
37
|
+
else if (requestedModel === 'seedream') {
|
|
38
|
+
resolvedModel = hasReferences ? 'seedream/4.5-edit' : 'seedream/4.5';
|
|
39
|
+
}
|
|
40
|
+
if (!hasReferences && (requestedModel === 'seedream' || requestedModel === 'midjourney')) {
|
|
41
|
+
if (requestedModel === 'midjourney') {
|
|
42
|
+
resolvedModel = 'midjourney';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
let body = {};
|
|
46
|
+
if (resolvedModel.includes('flux-2/')) {
|
|
47
|
+
body = {
|
|
48
|
+
model: resolvedModel,
|
|
49
|
+
input: {
|
|
50
|
+
prompt,
|
|
51
|
+
aspect_ratio: ratio,
|
|
52
|
+
resolution,
|
|
53
|
+
output_format: 'jpeg', // Force lightweight
|
|
54
|
+
...(hasReferences ? { input_urls: references } : {}),
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
else if (resolvedModel === 'seedream/4.5' || resolvedModel === 'seedream/4.5-edit') {
|
|
59
|
+
body = {
|
|
60
|
+
model: resolvedModel,
|
|
61
|
+
input: {
|
|
62
|
+
prompt,
|
|
63
|
+
aspect_ratio: mapRatio(resolvedModel, ratio),
|
|
64
|
+
quality,
|
|
65
|
+
output_format: 'jpeg', // Force lightweight
|
|
66
|
+
...(hasReferences ? { image_urls: references } : {}),
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
else if (resolvedModel === 'nano-banana-pro') {
|
|
71
|
+
body = {
|
|
72
|
+
model: resolvedModel,
|
|
73
|
+
input: {
|
|
74
|
+
prompt,
|
|
75
|
+
aspect_ratio: ratio,
|
|
76
|
+
resolution,
|
|
77
|
+
output_format: 'jpg', // Force lightweight
|
|
78
|
+
...(hasReferences ? { image_input: references } : {}),
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
else if (resolvedModel.includes('google/nano-banana')) {
|
|
83
|
+
body = {
|
|
84
|
+
model: resolvedModel,
|
|
85
|
+
input: {
|
|
86
|
+
prompt,
|
|
87
|
+
image_size: ratio,
|
|
88
|
+
output_format: 'jpeg', // Force lightweight
|
|
89
|
+
...(hasReferences ? { image_urls: references } : {}),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
else if (resolvedModel === 'midjourney') {
|
|
94
|
+
body = {
|
|
95
|
+
taskType: hasReferences ? 'mj_img2img' : 'mj_txt2img',
|
|
96
|
+
speed: 'fast',
|
|
97
|
+
prompt,
|
|
98
|
+
fileUrls: references,
|
|
99
|
+
aspectRatio: ratio,
|
|
100
|
+
enableTranslation: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
body = {
|
|
105
|
+
model: 'bytedance/seedream-v4-edit',
|
|
106
|
+
input: {
|
|
107
|
+
image_urls: references,
|
|
108
|
+
prompt,
|
|
109
|
+
image_size: mapRatio('seedream', ratio),
|
|
110
|
+
output_format: 'jpeg',
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
url: resolveGenerationUrl(resolvedModel),
|
|
116
|
+
body,
|
|
117
|
+
requestedModel,
|
|
118
|
+
resolvedModel,
|
|
119
|
+
ratio,
|
|
120
|
+
references,
|
|
121
|
+
hasReferences,
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
const extractReferences = (raw, model) => {
|
|
125
|
+
if (!raw || typeof raw !== 'object')
|
|
126
|
+
return [];
|
|
127
|
+
const collection = raw;
|
|
128
|
+
const items = Array.isArray(collection.reference) ? collection.reference : [];
|
|
129
|
+
let limit = 8;
|
|
130
|
+
if (model === 'seedream')
|
|
131
|
+
limit = 14;
|
|
132
|
+
if (model.includes('nano-banana'))
|
|
133
|
+
limit = 10;
|
|
134
|
+
return items.map((entry) => String((entry === null || entry === void 0 ? void 0 : entry.url) || '').trim()).filter((v) => v.length > 0 && /^https?:\/\//i.test(v)).slice(0, limit);
|
|
135
|
+
};
|
|
136
|
+
class ComandosImage {
|
|
137
|
+
constructor() {
|
|
138
|
+
this.description = {
|
|
139
|
+
displayName: 'Comandos Media',
|
|
140
|
+
name: 'comandosMedia',
|
|
141
|
+
group: ['transform'],
|
|
142
|
+
version: 1,
|
|
143
|
+
description: 'Create image or video tasks in Comandos API',
|
|
144
|
+
defaults: { name: 'Comandos Image' },
|
|
145
|
+
icon: 'file:Image.png',
|
|
146
|
+
inputs: ['main'],
|
|
147
|
+
outputs: ['main'],
|
|
148
|
+
credentials: [{ name: 'comandosApi', required: true }],
|
|
149
|
+
properties: [
|
|
150
|
+
{
|
|
151
|
+
displayName: 'Operation',
|
|
152
|
+
name: 'operation',
|
|
153
|
+
type: 'options',
|
|
154
|
+
noDataExpression: true,
|
|
155
|
+
options: [
|
|
156
|
+
{ name: 'Create Task', value: 'create' },
|
|
157
|
+
{ name: 'Check Status', value: 'status' },
|
|
158
|
+
],
|
|
159
|
+
default: 'create',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
displayName: 'Model',
|
|
163
|
+
name: 'model',
|
|
164
|
+
type: 'options',
|
|
165
|
+
options: [
|
|
166
|
+
{ name: 'Seedream 4.5', value: 'seedream' },
|
|
167
|
+
{ name: 'Flux Pro', value: 'flux-pro' },
|
|
168
|
+
{ name: 'Nano Banana', value: 'nano-banana' },
|
|
169
|
+
{ name: 'Nano Banana Pro', value: 'nano-banana-pro' },
|
|
170
|
+
{ name: 'Midjourney', value: 'midjourney' },
|
|
171
|
+
],
|
|
172
|
+
default: 'seedream',
|
|
173
|
+
displayOptions: { show: { operation: ['create'] } },
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
displayName: 'Prompt',
|
|
177
|
+
name: 'prompt',
|
|
178
|
+
type: 'string',
|
|
179
|
+
default: '',
|
|
180
|
+
typeOptions: { rows: 4 },
|
|
181
|
+
displayOptions: { show: { operation: ['create'] } },
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
displayName: 'Ratio',
|
|
185
|
+
name: 'ratio',
|
|
186
|
+
type: 'options',
|
|
187
|
+
options: [
|
|
188
|
+
{ name: '1:1', value: '1:1' },
|
|
189
|
+
{ name: '2:3', value: '2:3' },
|
|
190
|
+
{ name: '3:2', value: '3:2' },
|
|
191
|
+
{ name: '4:5', value: '4:5' },
|
|
192
|
+
{ name: '16:9', value: '16:9' },
|
|
193
|
+
{ name: '9:16', value: '9:16' },
|
|
194
|
+
{ name: '4:3', value: '4:3' },
|
|
195
|
+
{ name: '3:4', value: '3:4' },
|
|
196
|
+
{ name: '21:9', value: '21:9' },
|
|
197
|
+
],
|
|
198
|
+
default: '2:3',
|
|
199
|
+
displayOptions: {
|
|
200
|
+
show: { operation: ['create'] },
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
displayName: 'Quality',
|
|
205
|
+
name: 'quality',
|
|
206
|
+
type: 'options',
|
|
207
|
+
options: [
|
|
208
|
+
{ name: 'Basic (2K)', value: 'basic' },
|
|
209
|
+
{ name: 'High (4K)', value: 'high' },
|
|
210
|
+
{ name: 'Medium (Balanced)', value: 'medium' },
|
|
211
|
+
],
|
|
212
|
+
default: 'basic',
|
|
213
|
+
displayOptions: {
|
|
214
|
+
show: { operation: ['create'], model: ['seedream', 'flux-pro'] },
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
displayName: 'Resolution',
|
|
219
|
+
name: 'resolution',
|
|
220
|
+
type: 'options',
|
|
221
|
+
options: [
|
|
222
|
+
{ name: '1K', value: '1K' },
|
|
223
|
+
{ name: '2K', value: '2K' },
|
|
224
|
+
{ name: '4K', value: '4K' },
|
|
225
|
+
],
|
|
226
|
+
default: '1K',
|
|
227
|
+
displayOptions: {
|
|
228
|
+
show: { operation: ['create'], model: ['flux-pro', 'nano-banana-pro'] },
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
displayName: 'References',
|
|
233
|
+
name: 'references',
|
|
234
|
+
type: 'fixedCollection',
|
|
235
|
+
default: {},
|
|
236
|
+
typeOptions: { multipleValues: true },
|
|
237
|
+
options: [
|
|
238
|
+
{
|
|
239
|
+
name: 'reference',
|
|
240
|
+
displayName: 'Reference',
|
|
241
|
+
values: [{ displayName: 'Reference URL', name: 'url', type: 'string', default: '' }],
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
displayOptions: { show: { operation: ['create'] } },
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
displayName: 'Task ID',
|
|
248
|
+
name: 'taskId',
|
|
249
|
+
type: 'string',
|
|
250
|
+
default: '',
|
|
251
|
+
displayOptions: { show: { operation: ['status'] } },
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
async execute() {
|
|
257
|
+
const items = this.getInputData();
|
|
258
|
+
const operation = this.getNodeParameter('operation', 0);
|
|
259
|
+
const credentials = await this.getCredentials('comandosApi');
|
|
260
|
+
const licenseKey = String(credentials.licenseKey || '').trim();
|
|
261
|
+
const kieApiKey = String(credentials.kieApiKey || '').trim();
|
|
262
|
+
if (!licenseKey)
|
|
263
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'License key is required');
|
|
264
|
+
const results = [];
|
|
265
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
266
|
+
try {
|
|
267
|
+
if (operation === 'create') {
|
|
268
|
+
const model = String(this.getNodeParameter('model', i));
|
|
269
|
+
const prompt = String(this.getNodeParameter('prompt', i) || '').trim();
|
|
270
|
+
const ratio = String(this.getNodeParameter('ratio', i));
|
|
271
|
+
const references = extractReferences(this.getNodeParameter('references', i, {}), model);
|
|
272
|
+
const quality = ['seedream', 'flux-pro'].includes(model) ? String(this.getNodeParameter('quality', i, 'basic')) : 'basic';
|
|
273
|
+
const resolution = ['flux-pro', 'nano-banana-pro'].includes(model) ? String(this.getNodeParameter('resolution', i, '1K')) : '1K';
|
|
274
|
+
if (!prompt)
|
|
275
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Prompt is required', { itemIndex: i });
|
|
276
|
+
const request = buildGenerationRequest({ model, prompt, ratio, references, quality, resolution });
|
|
277
|
+
const payload = { ...request, licenseKey, _v: '0.1.0' };
|
|
278
|
+
const response = await this.helpers.request({
|
|
279
|
+
method: 'POST',
|
|
280
|
+
url: `${COMANDOS_API_URL}/tasks`,
|
|
281
|
+
headers: { 'Content-Type': 'application/json', 'X-License-Key': licenseKey },
|
|
282
|
+
body: { process_type: 'IMAGE_GENERATION', payload },
|
|
283
|
+
json: true,
|
|
284
|
+
});
|
|
285
|
+
results.push({ json: response });
|
|
286
|
+
}
|
|
287
|
+
else if (operation === 'status') {
|
|
288
|
+
const taskId = String(this.getNodeParameter('taskId', i) || '').trim();
|
|
289
|
+
if (!taskId)
|
|
290
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Task ID is required', { itemIndex: i });
|
|
291
|
+
const response = await this.helpers.request({
|
|
292
|
+
method: 'GET',
|
|
293
|
+
url: `${COMANDOS_API_URL}/tasks/${encodeURIComponent(taskId)}`,
|
|
294
|
+
headers: { 'X-License-Key': licenseKey },
|
|
295
|
+
json: true,
|
|
296
|
+
});
|
|
297
|
+
results.push({ json: response });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
if (error instanceof n8n_workflow_1.NodeOperationError)
|
|
302
|
+
throw error;
|
|
303
|
+
const message = error instanceof Error ? error.message : 'Request failed';
|
|
304
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), message, { itemIndex: i });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return [results];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
exports.ComandosImage = ComandosImage;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
2
|
+
export declare class ComandosMedia implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
5
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ComandosMedia = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
const COMANDOS_API_URL = String(process.env.COMANDOS_API_BASE_URL || process.env.COMMANDOS_API_BASE_URL || "https://api.comandos.ai").trim();
|
|
6
|
+
const GENERATION_URL_DEFAULT = String(process.env.COMANDOS_IMAGE_URL_DEFAULT || process.env.COMMANDOS_IMAGE_URL_DEFAULT || "").trim();
|
|
7
|
+
const GENERATION_URL_MJ = String(process.env.COMANDOS_IMAGE_URL_MJ || process.env.COMMANDOS_IMAGE_URL_MJ || "").trim();
|
|
8
|
+
const resolveGenerationUrl = (resolvedModel) => {
|
|
9
|
+
if (resolvedModel === 'midjourney') {
|
|
10
|
+
return GENERATION_URL_MJ || GENERATION_URL_DEFAULT;
|
|
11
|
+
}
|
|
12
|
+
return GENERATION_URL_DEFAULT;
|
|
13
|
+
};
|
|
14
|
+
const mapRatio = (model, ratio) => {
|
|
15
|
+
const isSeedream = model.includes('seedream');
|
|
16
|
+
if (isSeedream) {
|
|
17
|
+
if (ratio === '2:3')
|
|
18
|
+
return 'portrait_3_2';
|
|
19
|
+
if (ratio === '3:2')
|
|
20
|
+
return 'landscape_2_3';
|
|
21
|
+
}
|
|
22
|
+
return ratio;
|
|
23
|
+
};
|
|
24
|
+
const buildGenerationRequest = ({ model, prompt, ratio, references, quality = 'basic', resolution = '1K', }) => {
|
|
25
|
+
const hasReferences = references.length > 0;
|
|
26
|
+
const requestedModel = model;
|
|
27
|
+
let resolvedModel = model;
|
|
28
|
+
if (requestedModel === 'flux-pro') {
|
|
29
|
+
resolvedModel = hasReferences ? 'flux-2/pro-image-to-image' : 'flux-2/pro-text-to-image';
|
|
30
|
+
}
|
|
31
|
+
else if (requestedModel === 'nano-banana') {
|
|
32
|
+
resolvedModel = hasReferences ? 'google/nano-banana-edit' : 'google/nano-banana';
|
|
33
|
+
}
|
|
34
|
+
else if (requestedModel === 'nano-banana-pro') {
|
|
35
|
+
resolvedModel = 'nano-banana-pro';
|
|
36
|
+
}
|
|
37
|
+
else if (requestedModel === 'seedream') {
|
|
38
|
+
resolvedModel = hasReferences ? 'seedream/4.5-edit' : 'seedream/4.5';
|
|
39
|
+
}
|
|
40
|
+
if (!hasReferences && (requestedModel === 'seedream' || requestedModel === 'midjourney')) {
|
|
41
|
+
if (requestedModel === 'midjourney') {
|
|
42
|
+
resolvedModel = 'midjourney';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
let body = {};
|
|
46
|
+
if (resolvedModel.includes('flux-2/')) {
|
|
47
|
+
body = {
|
|
48
|
+
model: resolvedModel,
|
|
49
|
+
input: {
|
|
50
|
+
prompt,
|
|
51
|
+
aspect_ratio: ratio,
|
|
52
|
+
resolution,
|
|
53
|
+
output_format: 'jpeg', // Force lightweight
|
|
54
|
+
...(hasReferences ? { input_urls: references } : {}),
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
else if (resolvedModel === 'seedream/4.5' || resolvedModel === 'seedream/4.5-edit') {
|
|
59
|
+
body = {
|
|
60
|
+
model: resolvedModel,
|
|
61
|
+
input: {
|
|
62
|
+
prompt,
|
|
63
|
+
aspect_ratio: mapRatio(resolvedModel, ratio),
|
|
64
|
+
quality,
|
|
65
|
+
output_format: 'jpeg', // Force lightweight
|
|
66
|
+
...(hasReferences ? { image_urls: references } : {}),
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
else if (resolvedModel === 'nano-banana-pro') {
|
|
71
|
+
body = {
|
|
72
|
+
model: resolvedModel,
|
|
73
|
+
input: {
|
|
74
|
+
prompt,
|
|
75
|
+
aspect_ratio: ratio,
|
|
76
|
+
resolution,
|
|
77
|
+
output_format: 'jpg', // Force lightweight
|
|
78
|
+
...(hasReferences ? { image_input: references } : {}),
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
else if (resolvedModel.includes('google/nano-banana')) {
|
|
83
|
+
body = {
|
|
84
|
+
model: resolvedModel,
|
|
85
|
+
input: {
|
|
86
|
+
prompt,
|
|
87
|
+
image_size: ratio,
|
|
88
|
+
output_format: 'jpeg', // Force lightweight
|
|
89
|
+
...(hasReferences ? { image_urls: references } : {}),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
else if (resolvedModel === 'midjourney') {
|
|
94
|
+
body = {
|
|
95
|
+
taskType: hasReferences ? 'mj_img2img' : 'mj_txt2img',
|
|
96
|
+
speed: 'fast',
|
|
97
|
+
prompt,
|
|
98
|
+
fileUrls: references,
|
|
99
|
+
aspectRatio: ratio,
|
|
100
|
+
enableTranslation: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
body = {
|
|
105
|
+
model: 'bytedance/seedream-v4-edit',
|
|
106
|
+
input: {
|
|
107
|
+
image_urls: references,
|
|
108
|
+
prompt,
|
|
109
|
+
image_size: mapRatio('seedream', ratio),
|
|
110
|
+
output_format: 'jpeg',
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
url: resolveGenerationUrl(resolvedModel),
|
|
116
|
+
body,
|
|
117
|
+
requestedModel,
|
|
118
|
+
resolvedModel,
|
|
119
|
+
ratio,
|
|
120
|
+
references,
|
|
121
|
+
hasReferences,
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
const extractReferences = (raw, model) => {
|
|
125
|
+
if (!raw || typeof raw !== 'object')
|
|
126
|
+
return [];
|
|
127
|
+
const collection = raw;
|
|
128
|
+
const items = Array.isArray(collection.reference) ? collection.reference : [];
|
|
129
|
+
let limit = 8;
|
|
130
|
+
if (model === 'seedream')
|
|
131
|
+
limit = 14;
|
|
132
|
+
if (model.includes('nano-banana'))
|
|
133
|
+
limit = 10;
|
|
134
|
+
return items.map((entry) => String((entry === null || entry === void 0 ? void 0 : entry.url) || '').trim()).filter((v) => v.length > 0 && /^https?:\/\//i.test(v)).slice(0, limit);
|
|
135
|
+
};
|
|
136
|
+
class ComandosMedia {
|
|
137
|
+
constructor() {
|
|
138
|
+
this.description = {
|
|
139
|
+
displayName: 'Comandos Media',
|
|
140
|
+
name: 'comandosMedia',
|
|
141
|
+
group: ['transform'],
|
|
142
|
+
version: 1,
|
|
143
|
+
description: 'Create image or video tasks in Comandos API',
|
|
144
|
+
defaults: { name: 'Comandos Media' },
|
|
145
|
+
icon: 'file:Media.png',
|
|
146
|
+
inputs: ['main'],
|
|
147
|
+
outputs: ['main'],
|
|
148
|
+
credentials: [{ name: 'comandosApi', required: true }],
|
|
149
|
+
properties: [
|
|
150
|
+
{
|
|
151
|
+
displayName: 'Operation',
|
|
152
|
+
name: 'operation',
|
|
153
|
+
type: 'options',
|
|
154
|
+
noDataExpression: true,
|
|
155
|
+
options: [
|
|
156
|
+
{ name: 'Create Task', value: 'create' },
|
|
157
|
+
{ name: 'Check Status', value: 'status' },
|
|
158
|
+
],
|
|
159
|
+
default: 'create',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
displayName: 'Model',
|
|
163
|
+
name: 'model',
|
|
164
|
+
type: 'options',
|
|
165
|
+
options: [
|
|
166
|
+
{ name: 'Seedream 4.5', value: 'seedream' },
|
|
167
|
+
{ name: 'Flux Pro', value: 'flux-pro' },
|
|
168
|
+
{ name: 'Nano Banana', value: 'nano-banana' },
|
|
169
|
+
{ name: 'Nano Banana Pro', value: 'nano-banana-pro' },
|
|
170
|
+
{ name: 'Midjourney', value: 'midjourney' },
|
|
171
|
+
],
|
|
172
|
+
default: 'seedream',
|
|
173
|
+
displayOptions: { show: { operation: ['create'] } },
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
displayName: 'Prompt',
|
|
177
|
+
name: 'prompt',
|
|
178
|
+
type: 'string',
|
|
179
|
+
default: '',
|
|
180
|
+
typeOptions: { rows: 4 },
|
|
181
|
+
displayOptions: { show: { operation: ['create'] } },
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
displayName: 'Ratio',
|
|
185
|
+
name: 'ratio',
|
|
186
|
+
type: 'options',
|
|
187
|
+
options: [
|
|
188
|
+
{ name: '1:1', value: '1:1' },
|
|
189
|
+
{ name: '2:3', value: '2:3' },
|
|
190
|
+
{ name: '3:2', value: '3:2' },
|
|
191
|
+
{ name: '4:5', value: '4:5' },
|
|
192
|
+
{ name: '16:9', value: '16:9' },
|
|
193
|
+
{ name: '9:16', value: '9:16' },
|
|
194
|
+
{ name: '4:3', value: '4:3' },
|
|
195
|
+
{ name: '3:4', value: '3:4' },
|
|
196
|
+
{ name: '21:9', value: '21:9' },
|
|
197
|
+
],
|
|
198
|
+
default: '2:3',
|
|
199
|
+
displayOptions: {
|
|
200
|
+
show: { operation: ['create'] },
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
displayName: 'Quality',
|
|
205
|
+
name: 'quality',
|
|
206
|
+
type: 'options',
|
|
207
|
+
options: [
|
|
208
|
+
{ name: 'Basic (2K)', value: 'basic' },
|
|
209
|
+
{ name: 'High (4K)', value: 'high' },
|
|
210
|
+
{ name: 'Medium (Balanced)', value: 'medium' },
|
|
211
|
+
],
|
|
212
|
+
default: 'basic',
|
|
213
|
+
displayOptions: {
|
|
214
|
+
show: { operation: ['create'], model: ['seedream', 'flux-pro'] },
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
displayName: 'Resolution',
|
|
219
|
+
name: 'resolution',
|
|
220
|
+
type: 'options',
|
|
221
|
+
options: [
|
|
222
|
+
{ name: '1K', value: '1K' },
|
|
223
|
+
{ name: '2K', value: '2K' },
|
|
224
|
+
{ name: '4K', value: '4K' },
|
|
225
|
+
],
|
|
226
|
+
default: '1K',
|
|
227
|
+
displayOptions: {
|
|
228
|
+
show: { operation: ['create'], model: ['flux-pro', 'nano-banana-pro'] },
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
displayName: 'References',
|
|
233
|
+
name: 'references',
|
|
234
|
+
type: 'fixedCollection',
|
|
235
|
+
default: {},
|
|
236
|
+
typeOptions: { multipleValues: true },
|
|
237
|
+
options: [
|
|
238
|
+
{
|
|
239
|
+
name: 'reference',
|
|
240
|
+
displayName: 'Reference',
|
|
241
|
+
values: [{ displayName: 'Reference URL', name: 'url', type: 'string', default: '' }],
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
displayOptions: { show: { operation: ['create'] } },
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
displayName: 'Task ID',
|
|
248
|
+
name: 'taskId',
|
|
249
|
+
type: 'string',
|
|
250
|
+
default: '',
|
|
251
|
+
displayOptions: { show: { operation: ['status'] } },
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
async execute() {
|
|
257
|
+
const items = this.getInputData();
|
|
258
|
+
const operation = this.getNodeParameter('operation', 0);
|
|
259
|
+
const credentials = await this.getCredentials('comandosApi');
|
|
260
|
+
const licenseKey = String(credentials.licenseKey || '').trim();
|
|
261
|
+
const kieApiKey = String(credentials.kieApiKey || '').trim();
|
|
262
|
+
if (!licenseKey)
|
|
263
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'License key is required');
|
|
264
|
+
const results = [];
|
|
265
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
266
|
+
try {
|
|
267
|
+
if (operation === 'create') {
|
|
268
|
+
const model = String(this.getNodeParameter('model', i));
|
|
269
|
+
const prompt = String(this.getNodeParameter('prompt', i) || '').trim();
|
|
270
|
+
const ratio = String(this.getNodeParameter('ratio', i));
|
|
271
|
+
const references = extractReferences(this.getNodeParameter('references', i, {}), model);
|
|
272
|
+
const quality = ['seedream', 'flux-pro'].includes(model) ? String(this.getNodeParameter('quality', i, 'basic')) : 'basic';
|
|
273
|
+
const resolution = ['flux-pro', 'nano-banana-pro'].includes(model) ? String(this.getNodeParameter('resolution', i, '1K')) : '1K';
|
|
274
|
+
if (!prompt)
|
|
275
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Prompt is required', { itemIndex: i });
|
|
276
|
+
const request = buildGenerationRequest({ model, prompt, ratio, references, quality, resolution });
|
|
277
|
+
const payload = { ...request, licenseKey, kieApiKey, _v: '0.1.0' };
|
|
278
|
+
const response = await this.helpers.request({
|
|
279
|
+
method: 'POST',
|
|
280
|
+
url: `${COMANDOS_API_URL}/tasks`,
|
|
281
|
+
headers: { 'Content-Type': 'application/json', 'X-License-Key': licenseKey },
|
|
282
|
+
body: { process_type: 'IMAGE_GENERATION', payload },
|
|
283
|
+
json: true,
|
|
284
|
+
});
|
|
285
|
+
results.push({ json: response });
|
|
286
|
+
}
|
|
287
|
+
else if (operation === 'status') {
|
|
288
|
+
const taskId = String(this.getNodeParameter('taskId', i) || '').trim();
|
|
289
|
+
if (!taskId)
|
|
290
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Task ID is required', { itemIndex: i });
|
|
291
|
+
const response = await this.helpers.request({
|
|
292
|
+
method: 'GET',
|
|
293
|
+
url: `${COMANDOS_API_URL}/tasks/${encodeURIComponent(taskId)}`,
|
|
294
|
+
headers: { 'X-License-Key': licenseKey },
|
|
295
|
+
json: true,
|
|
296
|
+
});
|
|
297
|
+
results.push({ json: response });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
if (error instanceof n8n_workflow_1.NodeOperationError)
|
|
302
|
+
throw error;
|
|
303
|
+
const message = error instanceof Error ? error.message : 'Request failed';
|
|
304
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), message, { itemIndex: i });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return [results];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
exports.ComandosMedia = ComandosMedia;
|
|
Binary file
|
|
Binary file
|
package/index.ts
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IDataObject,
|
|
3
|
+
IExecuteFunctions,
|
|
4
|
+
INodeExecutionData,
|
|
5
|
+
INodeType,
|
|
6
|
+
INodeTypeDescription,
|
|
7
|
+
} from 'n8n-workflow';
|
|
8
|
+
import { NodeOperationError } from 'n8n-workflow';
|
|
9
|
+
|
|
10
|
+
const COMANDOS_API_URL = String(process.env.COMANDOS_API_BASE_URL || process.env.COMMANDOS_API_BASE_URL || "https://api.comandos.ai").trim();
|
|
11
|
+
const GENERATION_URL_DEFAULT = String(process.env.COMANDOS_IMAGE_URL_DEFAULT || process.env.COMMANDOS_IMAGE_URL_DEFAULT || "").trim();
|
|
12
|
+
const GENERATION_URL_MJ = String(process.env.COMANDOS_IMAGE_URL_MJ || process.env.COMMANDOS_IMAGE_URL_MJ || "").trim();
|
|
13
|
+
|
|
14
|
+
type BuildParams = {
|
|
15
|
+
model: string;
|
|
16
|
+
prompt: string;
|
|
17
|
+
ratio: string;
|
|
18
|
+
references: string[];
|
|
19
|
+
quality?: string;
|
|
20
|
+
resolution?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type BuildResult = {
|
|
24
|
+
url: string;
|
|
25
|
+
body: Record<string, unknown>;
|
|
26
|
+
requestedModel: string;
|
|
27
|
+
resolvedModel: string;
|
|
28
|
+
ratio: string;
|
|
29
|
+
references: string[];
|
|
30
|
+
hasReferences: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const resolveGenerationUrl = (resolvedModel: string): string => {
|
|
34
|
+
if (resolvedModel === 'midjourney') {
|
|
35
|
+
return GENERATION_URL_MJ || GENERATION_URL_DEFAULT;
|
|
36
|
+
}
|
|
37
|
+
return GENERATION_URL_DEFAULT;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const mapRatio = (model: string, ratio: string): string => {
|
|
41
|
+
const isSeedream = model.includes('seedream');
|
|
42
|
+
|
|
43
|
+
if (isSeedream) {
|
|
44
|
+
if (ratio === '2:3') return 'portrait_3_2';
|
|
45
|
+
if (ratio === '3:2') return 'landscape_2_3';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return ratio;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const buildGenerationRequest = ({
|
|
52
|
+
model,
|
|
53
|
+
prompt,
|
|
54
|
+
ratio,
|
|
55
|
+
references,
|
|
56
|
+
quality = 'basic',
|
|
57
|
+
resolution = '1K',
|
|
58
|
+
}: BuildParams): BuildResult => {
|
|
59
|
+
const hasReferences = references.length > 0;
|
|
60
|
+
const requestedModel = model;
|
|
61
|
+
let resolvedModel = model;
|
|
62
|
+
|
|
63
|
+
if (requestedModel === 'flux-pro') {
|
|
64
|
+
resolvedModel = hasReferences ? 'flux-2/pro-image-to-image' : 'flux-2/pro-text-to-image';
|
|
65
|
+
} else if (requestedModel === 'nano-banana') {
|
|
66
|
+
resolvedModel = hasReferences ? 'google/nano-banana-edit' : 'google/nano-banana';
|
|
67
|
+
} else if (requestedModel === 'nano-banana-pro') {
|
|
68
|
+
resolvedModel = 'nano-banana-pro';
|
|
69
|
+
} else if (requestedModel === 'seedream') {
|
|
70
|
+
resolvedModel = hasReferences ? 'seedream/4.5-edit' : 'seedream/4.5';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!hasReferences && (requestedModel === 'seedream' || requestedModel === 'midjourney')) {
|
|
74
|
+
if (requestedModel === 'midjourney') {
|
|
75
|
+
resolvedModel = 'midjourney';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let body: Record<string, unknown> = {};
|
|
80
|
+
|
|
81
|
+
if (resolvedModel.includes('flux-2/')) {
|
|
82
|
+
body = {
|
|
83
|
+
model: resolvedModel,
|
|
84
|
+
input: {
|
|
85
|
+
prompt,
|
|
86
|
+
aspect_ratio: ratio,
|
|
87
|
+
resolution,
|
|
88
|
+
output_format: 'jpeg', // Force lightweight
|
|
89
|
+
...(hasReferences ? { input_urls: references } : {}),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
} else if (resolvedModel === 'seedream/4.5' || resolvedModel === 'seedream/4.5-edit') {
|
|
93
|
+
body = {
|
|
94
|
+
model: resolvedModel,
|
|
95
|
+
input: {
|
|
96
|
+
prompt,
|
|
97
|
+
aspect_ratio: mapRatio(resolvedModel, ratio),
|
|
98
|
+
quality,
|
|
99
|
+
output_format: 'jpeg', // Force lightweight
|
|
100
|
+
...(hasReferences ? { image_urls: references } : {}),
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
} else if (resolvedModel === 'nano-banana-pro') {
|
|
104
|
+
body = {
|
|
105
|
+
model: resolvedModel,
|
|
106
|
+
input: {
|
|
107
|
+
prompt,
|
|
108
|
+
aspect_ratio: ratio,
|
|
109
|
+
resolution,
|
|
110
|
+
output_format: 'jpg', // Force lightweight
|
|
111
|
+
...(hasReferences ? { image_input: references } : {}),
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
} else if (resolvedModel.includes('google/nano-banana')) {
|
|
115
|
+
body = {
|
|
116
|
+
model: resolvedModel,
|
|
117
|
+
input: {
|
|
118
|
+
prompt,
|
|
119
|
+
image_size: ratio,
|
|
120
|
+
output_format: 'jpeg', // Force lightweight
|
|
121
|
+
...(hasReferences ? { image_urls: references } : {}),
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
} else if (resolvedModel === 'midjourney') {
|
|
125
|
+
body = {
|
|
126
|
+
taskType: hasReferences ? 'mj_img2img' : 'mj_txt2img',
|
|
127
|
+
speed: 'fast',
|
|
128
|
+
prompt,
|
|
129
|
+
fileUrls: references,
|
|
130
|
+
aspectRatio: ratio,
|
|
131
|
+
enableTranslation: true,
|
|
132
|
+
};
|
|
133
|
+
} else {
|
|
134
|
+
body = {
|
|
135
|
+
model: 'bytedance/seedream-v4-edit',
|
|
136
|
+
input: {
|
|
137
|
+
image_urls: references,
|
|
138
|
+
prompt,
|
|
139
|
+
image_size: mapRatio('seedream', ratio),
|
|
140
|
+
output_format: 'jpeg',
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
url: resolveGenerationUrl(resolvedModel),
|
|
147
|
+
body,
|
|
148
|
+
requestedModel,
|
|
149
|
+
resolvedModel,
|
|
150
|
+
ratio,
|
|
151
|
+
references,
|
|
152
|
+
hasReferences,
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const extractReferences = (raw: unknown, model: string): string[] => {
|
|
157
|
+
if (!raw || typeof raw !== 'object') return [];
|
|
158
|
+
const collection = raw as { reference?: Array<{ url?: string }> };
|
|
159
|
+
const items = Array.isArray(collection.reference) ? collection.reference : [];
|
|
160
|
+
let limit = 8;
|
|
161
|
+
if (model === 'seedream') limit = 14;
|
|
162
|
+
if (model.includes('nano-banana')) limit = 10;
|
|
163
|
+
return items.map((entry) => String(entry?.url || '').trim()).filter((v) => v.length > 0 && /^https?:\/\//i.test(v)).slice(0, limit);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export class ComandosMedia implements INodeType {
|
|
167
|
+
description: INodeTypeDescription = {
|
|
168
|
+
displayName: 'Comandos Media',
|
|
169
|
+
name: 'comandosMedia',
|
|
170
|
+
group: ['transform'],
|
|
171
|
+
version: 1,
|
|
172
|
+
description: 'Create image or video tasks in Comandos API',
|
|
173
|
+
defaults: { name: 'Comandos Media' },
|
|
174
|
+
icon: 'file:Media.png',
|
|
175
|
+
inputs: ['main'],
|
|
176
|
+
outputs: ['main'],
|
|
177
|
+
credentials: [{ name: 'comandosApi', required: true }],
|
|
178
|
+
properties: [
|
|
179
|
+
{
|
|
180
|
+
displayName: 'Operation',
|
|
181
|
+
name: 'operation',
|
|
182
|
+
type: 'options',
|
|
183
|
+
noDataExpression: true,
|
|
184
|
+
options: [
|
|
185
|
+
{ name: 'Create Task', value: 'create' },
|
|
186
|
+
{ name: 'Check Status', value: 'status' },
|
|
187
|
+
],
|
|
188
|
+
default: 'create',
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
displayName: 'Model',
|
|
192
|
+
name: 'model',
|
|
193
|
+
type: 'options',
|
|
194
|
+
options: [
|
|
195
|
+
{ name: 'Seedream 4.5', value: 'seedream' },
|
|
196
|
+
{ name: 'Flux Pro', value: 'flux-pro' },
|
|
197
|
+
{ name: 'Nano Banana', value: 'nano-banana' },
|
|
198
|
+
{ name: 'Nano Banana Pro', value: 'nano-banana-pro' },
|
|
199
|
+
{ name: 'Midjourney', value: 'midjourney' },
|
|
200
|
+
],
|
|
201
|
+
default: 'seedream',
|
|
202
|
+
displayOptions: { show: { operation: ['create'] } },
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
displayName: 'Prompt',
|
|
206
|
+
name: 'prompt',
|
|
207
|
+
type: 'string',
|
|
208
|
+
default: '',
|
|
209
|
+
typeOptions: { rows: 4 },
|
|
210
|
+
displayOptions: { show: { operation: ['create'] } },
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
displayName: 'Ratio',
|
|
214
|
+
name: 'ratio',
|
|
215
|
+
type: 'options',
|
|
216
|
+
options: [
|
|
217
|
+
{ name: '1:1', value: '1:1' },
|
|
218
|
+
{ name: '2:3', value: '2:3' },
|
|
219
|
+
{ name: '3:2', value: '3:2' },
|
|
220
|
+
{ name: '4:5', value: '4:5' },
|
|
221
|
+
{ name: '16:9', value: '16:9' },
|
|
222
|
+
{ name: '9:16', value: '9:16' },
|
|
223
|
+
{ name: '4:3', value: '4:3' },
|
|
224
|
+
{ name: '3:4', value: '3:4' },
|
|
225
|
+
{ name: '21:9', value: '21:9' },
|
|
226
|
+
],
|
|
227
|
+
default: '2:3',
|
|
228
|
+
displayOptions: {
|
|
229
|
+
show: { operation: ['create'] },
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
displayName: 'Quality',
|
|
234
|
+
name: 'quality',
|
|
235
|
+
type: 'options',
|
|
236
|
+
options: [
|
|
237
|
+
{ name: 'Basic (2K)', value: 'basic' },
|
|
238
|
+
{ name: 'High (4K)', value: 'high' },
|
|
239
|
+
{ name: 'Medium (Balanced)', value: 'medium' },
|
|
240
|
+
],
|
|
241
|
+
default: 'basic',
|
|
242
|
+
displayOptions: {
|
|
243
|
+
show: { operation: ['create'], model: ['seedream', 'flux-pro'] },
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
displayName: 'Resolution',
|
|
248
|
+
name: 'resolution',
|
|
249
|
+
type: 'options',
|
|
250
|
+
options: [
|
|
251
|
+
{ name: '1K', value: '1K' },
|
|
252
|
+
{ name: '2K', value: '2K' },
|
|
253
|
+
{ name: '4K', value: '4K' },
|
|
254
|
+
],
|
|
255
|
+
default: '1K',
|
|
256
|
+
displayOptions: {
|
|
257
|
+
show: { operation: ['create'], model: ['flux-pro', 'nano-banana-pro'] },
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
displayName: 'References',
|
|
262
|
+
name: 'references',
|
|
263
|
+
type: 'fixedCollection',
|
|
264
|
+
default: {},
|
|
265
|
+
typeOptions: { multipleValues: true },
|
|
266
|
+
options: [
|
|
267
|
+
{
|
|
268
|
+
name: 'reference',
|
|
269
|
+
displayName: 'Reference',
|
|
270
|
+
values: [{ displayName: 'Reference URL', name: 'url', type: 'string', default: '' }],
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
displayOptions: { show: { operation: ['create'] } },
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
displayName: 'Task ID',
|
|
277
|
+
name: 'taskId',
|
|
278
|
+
type: 'string',
|
|
279
|
+
default: '',
|
|
280
|
+
displayOptions: { show: { operation: ['status'] } },
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
286
|
+
const items = this.getInputData();
|
|
287
|
+
const operation = this.getNodeParameter('operation', 0) as string;
|
|
288
|
+
const credentials = await this.getCredentials('comandosApi');
|
|
289
|
+
const licenseKey = String(credentials.licenseKey || '').trim();
|
|
290
|
+
const kieApiKey = String(credentials.kieApiKey || '').trim();
|
|
291
|
+
if (!licenseKey) throw new NodeOperationError(this.getNode(), 'License key is required');
|
|
292
|
+
const results: INodeExecutionData[] = [];
|
|
293
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
294
|
+
try {
|
|
295
|
+
if (operation === 'create') {
|
|
296
|
+
const model = String(this.getNodeParameter('model', i));
|
|
297
|
+
const prompt = String(this.getNodeParameter('prompt', i) || '').trim();
|
|
298
|
+
const ratio = String(this.getNodeParameter('ratio', i));
|
|
299
|
+
const references = extractReferences(this.getNodeParameter('references', i, {}), model);
|
|
300
|
+
const quality = ['seedream', 'flux-pro'].includes(model) ? String(this.getNodeParameter('quality', i, 'basic')) : 'basic';
|
|
301
|
+
const resolution = ['flux-pro', 'nano-banana-pro'].includes(model) ? String(this.getNodeParameter('resolution', i, '1K')) : '1K';
|
|
302
|
+
|
|
303
|
+
if (!prompt) throw new NodeOperationError(this.getNode(), 'Prompt is required', { itemIndex: i });
|
|
304
|
+
|
|
305
|
+
const request = buildGenerationRequest({ model, prompt, ratio, references, quality, resolution });
|
|
306
|
+
const payload = { ...request, licenseKey, kieApiKey, _v: '0.1.0' };
|
|
307
|
+
const response = await this.helpers.request({
|
|
308
|
+
method: 'POST',
|
|
309
|
+
url: `${COMANDOS_API_URL}/tasks`,
|
|
310
|
+
headers: { 'Content-Type': 'application/json', 'X-License-Key': licenseKey },
|
|
311
|
+
body: { process_type: 'IMAGE_GENERATION', payload },
|
|
312
|
+
json: true,
|
|
313
|
+
});
|
|
314
|
+
results.push({ json: response as IDataObject });
|
|
315
|
+
} else if (operation === 'status') {
|
|
316
|
+
const taskId = String(this.getNodeParameter('taskId', i) || '').trim();
|
|
317
|
+
if (!taskId) throw new NodeOperationError(this.getNode(), 'Task ID is required', { itemIndex: i });
|
|
318
|
+
const response = await this.helpers.request({
|
|
319
|
+
method: 'GET',
|
|
320
|
+
url: `${COMANDOS_API_URL}/tasks/${encodeURIComponent(taskId)}`,
|
|
321
|
+
headers: { 'X-License-Key': licenseKey },
|
|
322
|
+
json: true,
|
|
323
|
+
});
|
|
324
|
+
results.push({ json: response as IDataObject });
|
|
325
|
+
}
|
|
326
|
+
} catch (error) {
|
|
327
|
+
if (error instanceof NodeOperationError) throw error;
|
|
328
|
+
const message = error instanceof Error ? error.message : 'Request failed';
|
|
329
|
+
throw new NodeOperationError(this.getNode(), message, { itemIndex: i });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return [results];
|
|
333
|
+
}
|
|
334
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@comandosai/n8n-nodes-media",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Comandos Media Generator custom node (Image/Video)",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc -p tsconfig.json && mkdir -p dist/nodes && cp -f Media.png dist/nodes/"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"n8n-workflow": "^1.0.0"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/node": "^18.19.0",
|
|
15
|
+
"typescript": "^5.4.5"
|
|
16
|
+
},
|
|
17
|
+
"n8n": {
|
|
18
|
+
"nodes": [
|
|
19
|
+
"dist/nodes/ComandosMedia.node.js"
|
|
20
|
+
],
|
|
21
|
+
"credentials": [
|
|
22
|
+
"dist/credentials/ComandosApi.credentials.js"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2019",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"lib": ["ES2019"],
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": ".",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"skipLibCheck": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["nodes/**/*.ts", "credentials/**/*.ts", "index.ts"]
|
|
15
|
+
}
|