pabal-resource-mcp 1.5.3 → 1.5.4
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/bin/mcp-server.js +74 -27
- package/package.json +1 -1
package/dist/bin/mcp-server.js
CHANGED
|
@@ -3523,7 +3523,7 @@ function calculateAspectRatio(width, height) {
|
|
|
3523
3523
|
if (Math.abs(ratio - 4 / 3) < 0.1) return "4:3";
|
|
3524
3524
|
return ratio < 1 ? "9:16" : "16:9";
|
|
3525
3525
|
}
|
|
3526
|
-
async function translateImage(sourcePath, sourceLocale, targetLocale, outputPath) {
|
|
3526
|
+
async function translateImage(sourcePath, sourceLocale, targetLocale, outputPath, preserveWords) {
|
|
3527
3527
|
try {
|
|
3528
3528
|
const client = getGeminiClient();
|
|
3529
3529
|
const sourceLanguage = getLanguageName(sourceLocale);
|
|
@@ -3531,6 +3531,8 @@ async function translateImage(sourcePath, sourceLocale, targetLocale, outputPath
|
|
|
3531
3531
|
const { width, height } = await getImageDimensions(sourcePath);
|
|
3532
3532
|
const aspectRatio = calculateAspectRatio(width, height);
|
|
3533
3533
|
const { data: imageData, mimeType } = readImageAsBase64(sourcePath);
|
|
3534
|
+
const preserveInstruction = preserveWords && preserveWords.length > 0 ? `
|
|
3535
|
+
- Do NOT translate these words, keep them exactly as-is: ${preserveWords.join(", ")}` : "";
|
|
3534
3536
|
const prompt = `This is an app screenshot with text in ${sourceLanguage}.
|
|
3535
3537
|
Please translate ONLY the text/words in this image to ${targetLanguage}.
|
|
3536
3538
|
|
|
@@ -3540,7 +3542,7 @@ IMPORTANT INSTRUCTIONS:
|
|
|
3540
3542
|
- Maintain the same font style and text positioning as much as possible
|
|
3541
3543
|
- Do NOT add any new elements or remove existing design elements
|
|
3542
3544
|
- The output should look identical except the text language is ${targetLanguage}
|
|
3543
|
-
- Preserve all icons, images, and graphical elements exactly as they are`;
|
|
3545
|
+
- Preserve all icons, images, and graphical elements exactly as they are${preserveInstruction}`;
|
|
3544
3546
|
const chat = client.chats.create({
|
|
3545
3547
|
model: "gemini-3-pro-image-preview",
|
|
3546
3548
|
config: {
|
|
@@ -3604,7 +3606,7 @@ IMPORTANT INSTRUCTIONS:
|
|
|
3604
3606
|
};
|
|
3605
3607
|
}
|
|
3606
3608
|
}
|
|
3607
|
-
async function translateImagesWithProgress(translations, onProgress) {
|
|
3609
|
+
async function translateImagesWithProgress(translations, onProgress, preserveWords) {
|
|
3608
3610
|
let successful = 0;
|
|
3609
3611
|
let failed = 0;
|
|
3610
3612
|
const errors = [];
|
|
@@ -3626,7 +3628,8 @@ async function translateImagesWithProgress(translations, onProgress) {
|
|
|
3626
3628
|
translation.sourcePath,
|
|
3627
3629
|
translation.sourceLocale,
|
|
3628
3630
|
translation.targetLocale,
|
|
3629
|
-
translation.outputPath
|
|
3631
|
+
translation.outputPath,
|
|
3632
|
+
preserveWords
|
|
3630
3633
|
);
|
|
3631
3634
|
if (result.success) {
|
|
3632
3635
|
successful++;
|
|
@@ -3659,7 +3662,7 @@ async function getImageDimensions2(imagePath) {
|
|
|
3659
3662
|
height: metadata.height
|
|
3660
3663
|
};
|
|
3661
3664
|
}
|
|
3662
|
-
async function
|
|
3665
|
+
async function detectCornerColor(imagePath) {
|
|
3663
3666
|
const image = sharp2(imagePath);
|
|
3664
3667
|
const metadata = await image.metadata();
|
|
3665
3668
|
const width = metadata.width || 100;
|
|
@@ -3667,14 +3670,17 @@ async function detectEdgeColor(imagePath) {
|
|
|
3667
3670
|
const { data, info } = await image.raw().toBuffer({ resolveWithObject: true });
|
|
3668
3671
|
const channels = info.channels;
|
|
3669
3672
|
const colorCounts = /* @__PURE__ */ new Map();
|
|
3670
|
-
const
|
|
3673
|
+
const cornerWidth = Math.min(100, Math.max(10, Math.floor(width * 0.05)));
|
|
3674
|
+
const cornerHeight = Math.min(100, Math.max(10, Math.floor(height * 0.05)));
|
|
3675
|
+
const samplePixel = (x, y) => {
|
|
3676
|
+
if (x < 0 || x >= width || y < 0 || y >= height) return;
|
|
3671
3677
|
const idx = (y * width + x) * channels;
|
|
3672
3678
|
const r = data[idx];
|
|
3673
3679
|
const g = data[idx + 1];
|
|
3674
3680
|
const b = data[idx + 2];
|
|
3675
|
-
const qr = Math.round(r /
|
|
3676
|
-
const qg = Math.round(g /
|
|
3677
|
-
const qb = Math.round(b /
|
|
3681
|
+
const qr = Math.round(r / 8) * 8;
|
|
3682
|
+
const qg = Math.round(g / 8) * 8;
|
|
3683
|
+
const qb = Math.round(b / 8) * 8;
|
|
3678
3684
|
const key = `${qr},${qg},${qb}`;
|
|
3679
3685
|
const existing = colorCounts.get(key);
|
|
3680
3686
|
if (existing) {
|
|
@@ -3683,13 +3689,22 @@ async function detectEdgeColor(imagePath) {
|
|
|
3683
3689
|
colorCounts.set(key, { count: 1, color: { r: qr, g: qg, b: qb } });
|
|
3684
3690
|
}
|
|
3685
3691
|
};
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3692
|
+
const corners = [
|
|
3693
|
+
{ startX: 0, startY: 0 },
|
|
3694
|
+
// Top-left
|
|
3695
|
+
{ startX: width - cornerWidth, startY: 0 },
|
|
3696
|
+
// Top-right
|
|
3697
|
+
{ startX: 0, startY: height - cornerHeight },
|
|
3698
|
+
// Bottom-left
|
|
3699
|
+
{ startX: width - cornerWidth, startY: height - cornerHeight }
|
|
3700
|
+
// Bottom-right
|
|
3701
|
+
];
|
|
3702
|
+
for (const corner of corners) {
|
|
3703
|
+
for (let y = corner.startY; y < corner.startY + cornerHeight; y += 2) {
|
|
3704
|
+
for (let x = corner.startX; x < corner.startX + cornerWidth; x += 2) {
|
|
3705
|
+
samplePixel(x, y);
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3693
3708
|
}
|
|
3694
3709
|
let maxCount = 0;
|
|
3695
3710
|
let dominantColor = { r: 255, g: 255, b: 255 };
|
|
@@ -3702,7 +3717,7 @@ async function detectEdgeColor(imagePath) {
|
|
|
3702
3717
|
return dominantColor;
|
|
3703
3718
|
}
|
|
3704
3719
|
async function resizeImage(inputPath, outputPath, targetDimensions) {
|
|
3705
|
-
const bgColor = await
|
|
3720
|
+
const bgColor = await detectCornerColor(inputPath);
|
|
3706
3721
|
await sharp2(inputPath).resize(targetDimensions.width, targetDimensions.height, {
|
|
3707
3722
|
fit: "contain",
|
|
3708
3723
|
// Preserve aspect ratio
|
|
@@ -3763,8 +3778,17 @@ var localizeScreenshotsInputSchema = z7.object({
|
|
|
3763
3778
|
deviceTypes: z7.array(z7.enum(["phone", "tablet"])).optional().default(["phone", "tablet"]).describe("Device types to process (default: both phone and tablet)"),
|
|
3764
3779
|
dryRun: z7.boolean().optional().default(false).describe("Preview mode - shows what would be translated without actually translating"),
|
|
3765
3780
|
skipExisting: z7.boolean().optional().default(true).describe("Skip translation if target file already exists (default: true)"),
|
|
3766
|
-
screenshotNumbers: z7.
|
|
3767
|
-
|
|
3781
|
+
screenshotNumbers: z7.union([
|
|
3782
|
+
z7.array(z7.number().int().positive()),
|
|
3783
|
+
z7.object({
|
|
3784
|
+
phone: z7.array(z7.number().int().positive()).optional(),
|
|
3785
|
+
tablet: z7.array(z7.number().int().positive()).optional()
|
|
3786
|
+
})
|
|
3787
|
+
]).optional().describe(
|
|
3788
|
+
"Specific screenshot numbers to process. Can be:\n- Array for all devices: [1, 3, 5]\n- Object for per-device: { phone: [1, 2], tablet: [1, 3, 5] }\nIf not provided, all screenshots will be processed."
|
|
3789
|
+
),
|
|
3790
|
+
preserveWords: z7.array(z7.string()).optional().describe(
|
|
3791
|
+
'Words to keep untranslated (e.g., brand names, product names). Example: ["Pabal", "Pro", "AI"]'
|
|
3768
3792
|
)
|
|
3769
3793
|
});
|
|
3770
3794
|
var jsonSchema7 = zodToJsonSchema7(localizeScreenshotsInputSchema, {
|
|
@@ -3881,7 +3905,8 @@ async function handleLocalizeScreenshots(input) {
|
|
|
3881
3905
|
deviceTypes = ["phone", "tablet"],
|
|
3882
3906
|
dryRun = false,
|
|
3883
3907
|
skipExisting = true,
|
|
3884
|
-
screenshotNumbers
|
|
3908
|
+
screenshotNumbers,
|
|
3909
|
+
preserveWords
|
|
3885
3910
|
} = input;
|
|
3886
3911
|
const results = [];
|
|
3887
3912
|
let appInfo;
|
|
@@ -3936,16 +3961,34 @@ async function handleLocalizeScreenshots(input) {
|
|
|
3936
3961
|
let filteredScreenshots = sourceScreenshots.filter(
|
|
3937
3962
|
(s) => deviceTypes.includes(s.type)
|
|
3938
3963
|
);
|
|
3939
|
-
if (screenshotNumbers
|
|
3964
|
+
if (screenshotNumbers) {
|
|
3965
|
+
const isArray = Array.isArray(screenshotNumbers);
|
|
3966
|
+
const phoneNumbers = isArray ? screenshotNumbers : screenshotNumbers.phone;
|
|
3967
|
+
const tabletNumbers = isArray ? screenshotNumbers : screenshotNumbers.tablet;
|
|
3940
3968
|
filteredScreenshots = filteredScreenshots.filter((s) => {
|
|
3941
3969
|
const match = s.filename.match(/^(\d+)\./);
|
|
3942
|
-
if (match)
|
|
3943
|
-
|
|
3944
|
-
|
|
3970
|
+
if (!match) return false;
|
|
3971
|
+
const num = parseInt(match[1], 10);
|
|
3972
|
+
const numbersForDevice = s.type === "phone" ? phoneNumbers : tabletNumbers;
|
|
3973
|
+
if (!numbersForDevice || numbersForDevice.length === 0) {
|
|
3974
|
+
return true;
|
|
3945
3975
|
}
|
|
3946
|
-
return
|
|
3976
|
+
return numbersForDevice.includes(num);
|
|
3947
3977
|
});
|
|
3948
|
-
|
|
3978
|
+
const filterParts = [];
|
|
3979
|
+
if (isArray) {
|
|
3980
|
+
filterParts.push(`all: ${screenshotNumbers.join(", ")}`);
|
|
3981
|
+
} else {
|
|
3982
|
+
if (phoneNumbers && phoneNumbers.length > 0) {
|
|
3983
|
+
filterParts.push(`phone: ${phoneNumbers.join(", ")}`);
|
|
3984
|
+
}
|
|
3985
|
+
if (tabletNumbers && tabletNumbers.length > 0) {
|
|
3986
|
+
filterParts.push(`tablet: ${tabletNumbers.join(", ")}`);
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
if (filterParts.length > 0) {
|
|
3990
|
+
results.push(`\u{1F522} Filtering screenshots: ${filterParts.join(" | ")}`);
|
|
3991
|
+
}
|
|
3949
3992
|
}
|
|
3950
3993
|
if (filteredScreenshots.length === 0) {
|
|
3951
3994
|
const screenshotsDir2 = getScreenshotsDir(appInfo.slug);
|
|
@@ -4015,6 +4058,9 @@ ${screenshotsDir2}/${primaryLocale}/tablet/1.png, 2.png, ...`
|
|
|
4015
4058
|
}
|
|
4016
4059
|
results.push(`
|
|
4017
4060
|
\u{1F680} Starting translations...`);
|
|
4061
|
+
if (preserveWords && preserveWords.length > 0) {
|
|
4062
|
+
results.push(`\u{1F512} Preserving words: ${preserveWords.join(", ")}`);
|
|
4063
|
+
}
|
|
4018
4064
|
const translationResult = await translateImagesWithProgress(
|
|
4019
4065
|
tasks,
|
|
4020
4066
|
(progress) => {
|
|
@@ -4032,7 +4078,8 @@ ${screenshotsDir2}/${primaryLocale}/tablet/1.png, 2.png, ...`
|
|
|
4032
4078
|
`\u274C ${progressPrefix} ${progress.targetLocale}/${progress.deviceType}/${progress.filename}: ${progress.error}`
|
|
4033
4079
|
);
|
|
4034
4080
|
}
|
|
4035
|
-
}
|
|
4081
|
+
},
|
|
4082
|
+
preserveWords
|
|
4036
4083
|
);
|
|
4037
4084
|
results.push(`
|
|
4038
4085
|
\u{1F4CA} Translation Results:`);
|