n8n-nodes-supermachine 0.7.0 → 0.7.1
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.
|
@@ -1379,8 +1379,164 @@ class Supermachine {
|
|
|
1379
1379
|
}
|
|
1380
1380
|
});
|
|
1381
1381
|
}
|
|
1382
|
+
else if (operation === 'faceSwap') {
|
|
1383
|
+
const faceImageSource = this.getNodeParameter('faceImageSource', i);
|
|
1384
|
+
const targetImageSource = this.getNodeParameter('targetImageSource', i);
|
|
1385
|
+
const additionalOptions = this.getNodeParameter('additionalOptions', i, {});
|
|
1386
|
+
let faceImageUrl = '';
|
|
1387
|
+
let targetImageUrl = '';
|
|
1388
|
+
// Step 1: Get face image URL
|
|
1389
|
+
if (faceImageSource === 'imageId') {
|
|
1390
|
+
const faceImageId = this.getNodeParameter('faceImageId', i);
|
|
1391
|
+
const faceImageDetails = await this.helpers.requestWithAuthentication.call(this, 'supermachineApi', {
|
|
1392
|
+
method: 'GET',
|
|
1393
|
+
url: `https://dev.supermachine.art/v1/images/${faceImageId}`,
|
|
1394
|
+
json: true,
|
|
1395
|
+
});
|
|
1396
|
+
faceImageUrl = faceImageDetails.url;
|
|
1397
|
+
if (!faceImageUrl) {
|
|
1398
|
+
throw new Error(`Face image ID ${faceImageId} does not have a valid URL`);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
else if (faceImageSource === 'imageUrl') {
|
|
1402
|
+
faceImageUrl = this.getNodeParameter('faceImageUrl', i);
|
|
1403
|
+
}
|
|
1404
|
+
// Step 2: Get target image URL
|
|
1405
|
+
if (targetImageSource === 'imageId') {
|
|
1406
|
+
const targetImageId = this.getNodeParameter('targetImageId', i);
|
|
1407
|
+
const targetImageDetails = await this.helpers.requestWithAuthentication.call(this, 'supermachineApi', {
|
|
1408
|
+
method: 'GET',
|
|
1409
|
+
url: `https://dev.supermachine.art/v1/images/${targetImageId}`,
|
|
1410
|
+
json: true,
|
|
1411
|
+
});
|
|
1412
|
+
targetImageUrl = targetImageDetails.url;
|
|
1413
|
+
if (!targetImageUrl) {
|
|
1414
|
+
throw new Error(`Target image ID ${targetImageId} does not have a valid URL`);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
else if (targetImageSource === 'imageUrl') {
|
|
1418
|
+
targetImageUrl = this.getNodeParameter('targetImageUrl', i);
|
|
1419
|
+
}
|
|
1420
|
+
// Step 3: Download face image as binary
|
|
1421
|
+
const faceImageBuffer = await this.helpers.httpRequest({
|
|
1422
|
+
method: 'GET',
|
|
1423
|
+
url: faceImageUrl,
|
|
1424
|
+
encoding: 'arraybuffer',
|
|
1425
|
+
returnFullResponse: false,
|
|
1426
|
+
});
|
|
1427
|
+
// Step 4: Download target image as binary
|
|
1428
|
+
const targetImageBuffer = await this.helpers.httpRequest({
|
|
1429
|
+
method: 'GET',
|
|
1430
|
+
url: targetImageUrl,
|
|
1431
|
+
encoding: 'arraybuffer',
|
|
1432
|
+
returnFullResponse: false,
|
|
1433
|
+
});
|
|
1434
|
+
// Step 5: Convert both images to base64
|
|
1435
|
+
const base64FaceImage = Buffer.from(faceImageBuffer).toString('base64');
|
|
1436
|
+
const base64TargetImage = Buffer.from(targetImageBuffer).toString('base64');
|
|
1437
|
+
// Detect MIME types
|
|
1438
|
+
let faceMimeType = 'image/jpeg';
|
|
1439
|
+
if (faceImageUrl.toLowerCase().includes('.png')) {
|
|
1440
|
+
faceMimeType = 'image/png';
|
|
1441
|
+
}
|
|
1442
|
+
else if (faceImageUrl.toLowerCase().includes('.webp')) {
|
|
1443
|
+
faceMimeType = 'image/webp';
|
|
1444
|
+
}
|
|
1445
|
+
let targetMimeType = 'image/jpeg';
|
|
1446
|
+
if (targetImageUrl.toLowerCase().includes('.png')) {
|
|
1447
|
+
targetMimeType = 'image/png';
|
|
1448
|
+
}
|
|
1449
|
+
else if (targetImageUrl.toLowerCase().includes('.webp')) {
|
|
1450
|
+
targetMimeType = 'image/webp';
|
|
1451
|
+
}
|
|
1452
|
+
const faceDataUri = `data:${faceMimeType};base64,${base64FaceImage}`;
|
|
1453
|
+
const targetDataUri = `data:${targetMimeType};base64,${base64TargetImage}`;
|
|
1454
|
+
// Step 6: Submit to face-swap API
|
|
1455
|
+
const body = {
|
|
1456
|
+
faceImage: faceDataUri,
|
|
1457
|
+
targetImage: targetDataUri,
|
|
1458
|
+
};
|
|
1459
|
+
// Add folderId if provided
|
|
1460
|
+
if (additionalOptions.folderId) {
|
|
1461
|
+
body.folderId = additionalOptions.folderId;
|
|
1462
|
+
}
|
|
1463
|
+
let faceSwapResponse;
|
|
1464
|
+
try {
|
|
1465
|
+
faceSwapResponse = await this.helpers.requestWithAuthentication.call(this, 'supermachineApi', {
|
|
1466
|
+
method: 'POST',
|
|
1467
|
+
url: 'https://dev.supermachine.art/v1/tools/faceswap',
|
|
1468
|
+
json: true,
|
|
1469
|
+
body,
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
catch (error) {
|
|
1473
|
+
throw new Error(`Failed to swap faces: ${error.message}`);
|
|
1474
|
+
}
|
|
1475
|
+
const batchId = faceSwapResponse.batchId;
|
|
1476
|
+
if (!batchId) {
|
|
1477
|
+
throw new Error('No batch ID returned from face-swap API');
|
|
1478
|
+
}
|
|
1479
|
+
// Step 7: Handle polling or return batch ID
|
|
1480
|
+
const skipPolling = additionalOptions.skipPolling;
|
|
1481
|
+
if (skipPolling) {
|
|
1482
|
+
returnData.push({
|
|
1483
|
+
json: {
|
|
1484
|
+
success: true,
|
|
1485
|
+
batchId,
|
|
1486
|
+
tool: 'faceswap',
|
|
1487
|
+
creditsCost: faceSwapResponse.creditsCost || 2,
|
|
1488
|
+
creditsRemaining: faceSwapResponse.creditsRemaining,
|
|
1489
|
+
operation: 'faceSwap',
|
|
1490
|
+
faceImage: faceImageUrl,
|
|
1491
|
+
targetImage: targetImageUrl,
|
|
1492
|
+
message: 'Face swap job submitted. Use "Get Batch Status" to check results.',
|
|
1493
|
+
...faceSwapResponse,
|
|
1494
|
+
},
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
else {
|
|
1498
|
+
// Poll for results
|
|
1499
|
+
const pollingInterval = additionalOptions.pollingInterval || 2;
|
|
1500
|
+
const maxPollingTime = additionalOptions.maxPollingTime || 120;
|
|
1501
|
+
const startTime = Date.now();
|
|
1502
|
+
let completed = false;
|
|
1503
|
+
let finalResult = null;
|
|
1504
|
+
while (!completed && (Date.now() - startTime) / 1000 < maxPollingTime) {
|
|
1505
|
+
await new Promise((resolve) => setTimeout(resolve, pollingInterval * 1000));
|
|
1506
|
+
const statusResponse = await this.helpers.requestWithAuthentication.call(this, 'supermachineApi', {
|
|
1507
|
+
method: 'GET',
|
|
1508
|
+
url: `https://dev.supermachine.art/v1/batches/${batchId}`,
|
|
1509
|
+
json: true,
|
|
1510
|
+
});
|
|
1511
|
+
const status = statusResponse.status;
|
|
1512
|
+
if (status === 'completed') {
|
|
1513
|
+
completed = true;
|
|
1514
|
+
finalResult = statusResponse;
|
|
1515
|
+
}
|
|
1516
|
+
else if (status === 'failed') {
|
|
1517
|
+
throw new Error(`Face swap failed: ${statusResponse.error || 'Unknown error'}`);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
if (!completed) {
|
|
1521
|
+
throw new Error(`Face swap timed out after ${maxPollingTime} seconds`);
|
|
1522
|
+
}
|
|
1523
|
+
returnData.push({
|
|
1524
|
+
json: {
|
|
1525
|
+
success: true,
|
|
1526
|
+
batchId,
|
|
1527
|
+
status: 'completed',
|
|
1528
|
+
operation: 'faceSwap',
|
|
1529
|
+
tool: 'faceswap',
|
|
1530
|
+
faceImage: faceImageUrl,
|
|
1531
|
+
targetImage: targetImageUrl,
|
|
1532
|
+
completedAt: new Date().toISOString(),
|
|
1533
|
+
...finalResult,
|
|
1534
|
+
},
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1382
1538
|
else {
|
|
1383
|
-
throw new Error(`⚠️ Operation "${operation}" is
|
|
1539
|
+
throw new Error(`⚠️ Operation "${operation}" is not yet implemented.`);
|
|
1384
1540
|
}
|
|
1385
1541
|
}
|
|
1386
1542
|
}
|
|
@@ -38,7 +38,7 @@ exports.getToolsOperations = [
|
|
|
38
38
|
action: 'Image to prompt',
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
|
-
name: 'Face Swap
|
|
41
|
+
name: 'Face Swap',
|
|
42
42
|
value: 'faceSwap',
|
|
43
43
|
description: 'Swap faces in images',
|
|
44
44
|
action: 'Face swap',
|
|
@@ -260,8 +260,8 @@ exports.getToolsFields = [
|
|
|
260
260
|
description: 'High quality 4x upscaling (recommended)',
|
|
261
261
|
},
|
|
262
262
|
{
|
|
263
|
-
name: 'R-ESRGAN 4x+
|
|
264
|
-
value: 'R-ESRGAN 4x+
|
|
263
|
+
name: 'R-ESRGAN 4x+ Anime6B',
|
|
264
|
+
value: 'R-ESRGAN 4x+ Anime6B',
|
|
265
265
|
description: 'Optimized for anime/cartoon images',
|
|
266
266
|
},
|
|
267
267
|
],
|
|
@@ -546,19 +546,176 @@ exports.getToolsFields = [
|
|
|
546
546
|
description: 'The URL of the image to analyze',
|
|
547
547
|
},
|
|
548
548
|
// ══════════════════════════════════════════════════════
|
|
549
|
-
//
|
|
549
|
+
// FACE SWAP FIELDS
|
|
550
550
|
// ══════════════════════════════════════════════════════
|
|
551
551
|
{
|
|
552
|
-
displayName: '
|
|
553
|
-
name: '
|
|
554
|
-
type: '
|
|
552
|
+
displayName: 'Face Image Source',
|
|
553
|
+
name: 'faceImageSource',
|
|
554
|
+
type: 'options',
|
|
555
|
+
displayOptions: {
|
|
556
|
+
show: {
|
|
557
|
+
resource: ['tools'],
|
|
558
|
+
operation: ['faceSwap'],
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
options: [
|
|
562
|
+
{
|
|
563
|
+
name: 'Image ID',
|
|
564
|
+
value: 'imageId',
|
|
565
|
+
description: 'Use an image already in Supermachine by its ID',
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
name: 'Image URL',
|
|
569
|
+
value: 'imageUrl',
|
|
570
|
+
description: 'Use an external image URL',
|
|
571
|
+
},
|
|
572
|
+
],
|
|
573
|
+
default: 'imageId',
|
|
574
|
+
description: 'Where to get the face image from (the face you want to use)',
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
displayName: 'Face Image ID',
|
|
578
|
+
name: 'faceImageId',
|
|
579
|
+
type: 'string',
|
|
580
|
+
displayOptions: {
|
|
581
|
+
show: {
|
|
582
|
+
resource: ['tools'],
|
|
583
|
+
operation: ['faceSwap'],
|
|
584
|
+
faceImageSource: ['imageId'],
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
required: true,
|
|
588
|
+
default: '',
|
|
589
|
+
placeholder: '12345',
|
|
590
|
+
description: 'The ID of the face image to use',
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
displayName: 'Face Image URL',
|
|
594
|
+
name: 'faceImageUrl',
|
|
595
|
+
type: 'string',
|
|
596
|
+
displayOptions: {
|
|
597
|
+
show: {
|
|
598
|
+
resource: ['tools'],
|
|
599
|
+
operation: ['faceSwap'],
|
|
600
|
+
faceImageSource: ['imageUrl'],
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
required: true,
|
|
604
|
+
default: '',
|
|
605
|
+
placeholder: 'https://example.com/face.jpg',
|
|
606
|
+
description: 'The URL of the face image to use',
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
displayName: 'Target Image Source',
|
|
610
|
+
name: 'targetImageSource',
|
|
611
|
+
type: 'options',
|
|
612
|
+
displayOptions: {
|
|
613
|
+
show: {
|
|
614
|
+
resource: ['tools'],
|
|
615
|
+
operation: ['faceSwap'],
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
options: [
|
|
619
|
+
{
|
|
620
|
+
name: 'Image ID',
|
|
621
|
+
value: 'imageId',
|
|
622
|
+
description: 'Use an image already in Supermachine by its ID',
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
name: 'Image URL',
|
|
626
|
+
value: 'imageUrl',
|
|
627
|
+
description: 'Use an external image URL',
|
|
628
|
+
},
|
|
629
|
+
],
|
|
630
|
+
default: 'imageId',
|
|
631
|
+
description: 'Where to get the target image from (the image where face will be swapped)',
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
displayName: 'Target Image ID',
|
|
635
|
+
name: 'targetImageId',
|
|
636
|
+
type: 'string',
|
|
555
637
|
displayOptions: {
|
|
556
638
|
show: {
|
|
557
639
|
resource: ['tools'],
|
|
558
640
|
operation: ['faceSwap'],
|
|
641
|
+
targetImageSource: ['imageId'],
|
|
559
642
|
},
|
|
560
643
|
},
|
|
644
|
+
required: true,
|
|
561
645
|
default: '',
|
|
562
|
-
|
|
646
|
+
placeholder: '67890',
|
|
647
|
+
description: 'The ID of the target image',
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
displayName: 'Target Image URL',
|
|
651
|
+
name: 'targetImageUrl',
|
|
652
|
+
type: 'string',
|
|
653
|
+
displayOptions: {
|
|
654
|
+
show: {
|
|
655
|
+
resource: ['tools'],
|
|
656
|
+
operation: ['faceSwap'],
|
|
657
|
+
targetImageSource: ['imageUrl'],
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
required: true,
|
|
661
|
+
default: '',
|
|
662
|
+
placeholder: 'https://example.com/target.jpg',
|
|
663
|
+
description: 'The URL of the target image',
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
displayName: 'Additional Options',
|
|
667
|
+
name: 'additionalOptions',
|
|
668
|
+
type: 'collection',
|
|
669
|
+
placeholder: 'Add Option',
|
|
670
|
+
default: {},
|
|
671
|
+
displayOptions: {
|
|
672
|
+
show: {
|
|
673
|
+
resource: ['tools'],
|
|
674
|
+
operation: ['faceSwap'],
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
options: [
|
|
678
|
+
{
|
|
679
|
+
displayName: 'Save to Folder',
|
|
680
|
+
name: 'folderId',
|
|
681
|
+
type: 'options',
|
|
682
|
+
typeOptions: {
|
|
683
|
+
loadOptionsMethod: 'getFolders',
|
|
684
|
+
},
|
|
685
|
+
default: '',
|
|
686
|
+
description: 'Save the result to a specific folder. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
displayName: 'Skip Polling',
|
|
690
|
+
name: 'skipPolling',
|
|
691
|
+
type: 'boolean',
|
|
692
|
+
default: false,
|
|
693
|
+
description: 'Whether to skip waiting for the result and return immediately with batch ID',
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
displayName: 'Polling Interval (seconds)',
|
|
697
|
+
name: 'pollingInterval',
|
|
698
|
+
type: 'number',
|
|
699
|
+
default: 2,
|
|
700
|
+
description: 'How often to check if face swap is complete (in seconds)',
|
|
701
|
+
displayOptions: {
|
|
702
|
+
show: {
|
|
703
|
+
skipPolling: [false],
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
displayName: 'Max Polling Time (seconds)',
|
|
709
|
+
name: 'maxPollingTime',
|
|
710
|
+
type: 'number',
|
|
711
|
+
default: 120,
|
|
712
|
+
description: 'Maximum time to wait for face swap to complete (in seconds)',
|
|
713
|
+
displayOptions: {
|
|
714
|
+
show: {
|
|
715
|
+
skipPolling: [false],
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
],
|
|
563
720
|
},
|
|
564
721
|
];
|
package/package.json
CHANGED