pabal-resource-mcp 1.8.11 → 1.9.0
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 +2 -2
- package/dist/bin/mcp-server.js +317 -275
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Build synced websites from App Store Connect and Google Play Console data.
|
|
|
6
6
|
|
|
7
7
|
> 💡 **Example**: [labs.quartz.best](https://labs.quartz.best/)
|
|
8
8
|
|
|
9
|
-
[](https://pabal.quartz.best/
|
|
9
|
+
[](https://pabal.quartz.best/en-US/docs/pabal-resource-mcp/README) [](https://pabal.quartz.best/ko-KR/docs/pabal-resource-mcp/README)
|
|
10
10
|
|
|
11
11
|
## Installation
|
|
12
12
|
|
|
@@ -61,7 +61,7 @@ Set `dataDir` in `~/.config/pabal-mcp/config.json`:
|
|
|
61
61
|
| App Icon | `generate-app-icons` |
|
|
62
62
|
| Content | `create-blog-html` |
|
|
63
63
|
|
|
64
|
-
See [documentation](
|
|
64
|
+
See [documentation](https://pabal.quartz.best/en-US/docs/pabal-resource-mcp/README) for details.
|
|
65
65
|
|
|
66
66
|
## License
|
|
67
67
|
|
package/dist/bin/mcp-server.js
CHANGED
|
@@ -3398,8 +3398,8 @@ Context around ${pos}: ${context}`
|
|
|
3398
3398
|
// src/tools/screenshots/translate-screenshots.ts
|
|
3399
3399
|
import { z as z7 } from "zod";
|
|
3400
3400
|
import { zodToJsonSchema as zodToJsonSchema7 } from "zod-to-json-schema";
|
|
3401
|
-
import
|
|
3402
|
-
import
|
|
3401
|
+
import fs12 from "fs";
|
|
3402
|
+
import path12 from "path";
|
|
3403
3403
|
|
|
3404
3404
|
// src/tools/screenshots/utils/scan-screenshots.util.ts
|
|
3405
3405
|
import fs9 from "fs";
|
|
@@ -3481,10 +3481,161 @@ function scanRawScreenshots(slug, locale) {
|
|
|
3481
3481
|
}
|
|
3482
3482
|
|
|
3483
3483
|
// src/tools/screenshots/utils/gemini-image-translator.util.ts
|
|
3484
|
+
import fs11 from "fs";
|
|
3485
|
+
import path11 from "path";
|
|
3486
|
+
import sharp from "sharp";
|
|
3487
|
+
|
|
3488
|
+
// src/utils/gemini-image-model.util.ts
|
|
3489
|
+
var GEMINI_IMAGE_MODEL_PRESETS = {
|
|
3490
|
+
flash: "gemini-3.1-flash-image-preview",
|
|
3491
|
+
pro: "gemini-3-pro-image-preview"
|
|
3492
|
+
};
|
|
3493
|
+
var GEMINI_IMAGE_MODEL_VALUES = Object.keys(
|
|
3494
|
+
GEMINI_IMAGE_MODEL_PRESETS
|
|
3495
|
+
);
|
|
3496
|
+
var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 503, 504]);
|
|
3497
|
+
function parseCsv(value) {
|
|
3498
|
+
if (!value) {
|
|
3499
|
+
return [];
|
|
3500
|
+
}
|
|
3501
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
3502
|
+
}
|
|
3503
|
+
function getStatusCode(error) {
|
|
3504
|
+
if (typeof error !== "object" || error === null) {
|
|
3505
|
+
return void 0;
|
|
3506
|
+
}
|
|
3507
|
+
const status = error.status;
|
|
3508
|
+
return typeof status === "number" ? status : void 0;
|
|
3509
|
+
}
|
|
3510
|
+
function getDefaultModelOrder(preference) {
|
|
3511
|
+
if (preference === "pro") {
|
|
3512
|
+
return [
|
|
3513
|
+
GEMINI_IMAGE_MODEL_PRESETS.pro,
|
|
3514
|
+
GEMINI_IMAGE_MODEL_PRESETS.flash
|
|
3515
|
+
];
|
|
3516
|
+
}
|
|
3517
|
+
return [
|
|
3518
|
+
GEMINI_IMAGE_MODEL_PRESETS.flash,
|
|
3519
|
+
GEMINI_IMAGE_MODEL_PRESETS.pro
|
|
3520
|
+
];
|
|
3521
|
+
}
|
|
3522
|
+
function getGeminiImageModelCandidates(preference = "flash") {
|
|
3523
|
+
const defaultOrder = getDefaultModelOrder(preference);
|
|
3524
|
+
const preferredModel = defaultOrder[0];
|
|
3525
|
+
const envModelOverride = process.env.GEMINI_IMAGE_MODEL?.trim();
|
|
3526
|
+
const fallbackModels = parseCsv(process.env.GEMINI_IMAGE_FALLBACK_MODELS);
|
|
3527
|
+
const orderedModels = [
|
|
3528
|
+
preferredModel,
|
|
3529
|
+
...envModelOverride ? [envModelOverride] : [],
|
|
3530
|
+
...defaultOrder,
|
|
3531
|
+
...fallbackModels
|
|
3532
|
+
];
|
|
3533
|
+
return Array.from(
|
|
3534
|
+
new Set(orderedModels.filter((model) => Boolean(model)))
|
|
3535
|
+
);
|
|
3536
|
+
}
|
|
3537
|
+
function getGeminiErrorMessage(error) {
|
|
3538
|
+
const defaultMessage = error instanceof Error ? error.message : String(error);
|
|
3539
|
+
const normalizedMessage = defaultMessage.replace(/^exception\s*/i, "").trim();
|
|
3540
|
+
try {
|
|
3541
|
+
const parsed = JSON.parse(normalizedMessage);
|
|
3542
|
+
const apiMessage = parsed?.error?.message;
|
|
3543
|
+
if (typeof apiMessage === "string" && apiMessage.trim()) {
|
|
3544
|
+
return apiMessage.trim();
|
|
3545
|
+
}
|
|
3546
|
+
} catch {
|
|
3547
|
+
}
|
|
3548
|
+
return normalizedMessage || "Unknown Gemini API error";
|
|
3549
|
+
}
|
|
3550
|
+
function shouldTryNextGeminiImageModel(error) {
|
|
3551
|
+
const status = getStatusCode(error);
|
|
3552
|
+
if (status && RETRYABLE_STATUS_CODES.has(status)) {
|
|
3553
|
+
return true;
|
|
3554
|
+
}
|
|
3555
|
+
const message = getGeminiErrorMessage(error).toLowerCase();
|
|
3556
|
+
return message.includes("high demand") || message.includes("temporarily unavailable") || message.includes("try again later") || message.includes("overloaded") || message.includes("resource_exhausted") || message.includes("unavailable");
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
// src/utils/gemini-client.util.ts
|
|
3484
3560
|
import { GoogleGenAI } from "@google/genai";
|
|
3561
|
+
function createGeminiClient() {
|
|
3562
|
+
const apiKey = getGeminiApiKey();
|
|
3563
|
+
return new GoogleGenAI({ apiKey });
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
// src/utils/image-file.util.ts
|
|
3485
3567
|
import fs10 from "fs";
|
|
3486
3568
|
import path10 from "path";
|
|
3487
|
-
|
|
3569
|
+
function readImageAsBase64(imagePath) {
|
|
3570
|
+
const buffer = fs10.readFileSync(imagePath);
|
|
3571
|
+
const base64 = buffer.toString("base64");
|
|
3572
|
+
const ext = path10.extname(imagePath).toLowerCase();
|
|
3573
|
+
let mimeType = "image/png";
|
|
3574
|
+
if (ext === ".jpg" || ext === ".jpeg") {
|
|
3575
|
+
mimeType = "image/jpeg";
|
|
3576
|
+
} else if (ext === ".webp") {
|
|
3577
|
+
mimeType = "image/webp";
|
|
3578
|
+
}
|
|
3579
|
+
return { data: base64, mimeType };
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
// src/utils/gemini-image-generation.util.ts
|
|
3583
|
+
async function generateImageWithFallback(input) {
|
|
3584
|
+
const models = getGeminiImageModelCandidates(input.imageModel);
|
|
3585
|
+
let lastError;
|
|
3586
|
+
for (const model of models) {
|
|
3587
|
+
try {
|
|
3588
|
+
const chat = input.client.chats.create({
|
|
3589
|
+
model,
|
|
3590
|
+
config: {
|
|
3591
|
+
responseModalities: ["TEXT", "IMAGE"]
|
|
3592
|
+
}
|
|
3593
|
+
});
|
|
3594
|
+
const response = await chat.sendMessage({
|
|
3595
|
+
message: [
|
|
3596
|
+
{ text: input.prompt },
|
|
3597
|
+
{
|
|
3598
|
+
inlineData: {
|
|
3599
|
+
mimeType: input.image.mimeType,
|
|
3600
|
+
data: input.image.data
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
],
|
|
3604
|
+
config: {
|
|
3605
|
+
responseModalities: ["TEXT", "IMAGE"],
|
|
3606
|
+
...input.aspectRatio ? {
|
|
3607
|
+
imageConfig: {
|
|
3608
|
+
aspectRatio: input.aspectRatio
|
|
3609
|
+
}
|
|
3610
|
+
} : {}
|
|
3611
|
+
}
|
|
3612
|
+
});
|
|
3613
|
+
const parts = response.candidates?.[0]?.content?.parts;
|
|
3614
|
+
if (!parts) {
|
|
3615
|
+
throw new Error("No content parts in Gemini response");
|
|
3616
|
+
}
|
|
3617
|
+
for (const part of parts) {
|
|
3618
|
+
if (part.inlineData?.data) {
|
|
3619
|
+
return {
|
|
3620
|
+
model,
|
|
3621
|
+
imageBase64: part.inlineData.data
|
|
3622
|
+
};
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
throw new Error("No image data in Gemini response");
|
|
3626
|
+
} catch (error) {
|
|
3627
|
+
lastError = error;
|
|
3628
|
+
if (!shouldTryNextGeminiImageModel(error)) {
|
|
3629
|
+
break;
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
throw new Error(
|
|
3634
|
+
`All Gemini image models failed (${models.join(", ")}): ${getGeminiErrorMessage(lastError)}`
|
|
3635
|
+
);
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
// src/tools/screenshots/utils/gemini-image-translator.util.ts
|
|
3488
3639
|
var GEMINI_ASPECT_RATIOS = {
|
|
3489
3640
|
"1:1": { ratio: 1 / 1, width: 2048, height: 2048 },
|
|
3490
3641
|
"2:3": { ratio: 2 / 3, width: 1696, height: 2528 },
|
|
@@ -3524,35 +3675,18 @@ function getLanguageName(locale) {
|
|
|
3524
3675
|
}
|
|
3525
3676
|
return locale;
|
|
3526
3677
|
}
|
|
3527
|
-
function getGeminiClient() {
|
|
3528
|
-
const apiKey = getGeminiApiKey();
|
|
3529
|
-
return new GoogleGenAI({ apiKey });
|
|
3530
|
-
}
|
|
3531
|
-
function readImageAsBase64(imagePath) {
|
|
3532
|
-
const buffer = fs10.readFileSync(imagePath);
|
|
3533
|
-
const base64 = buffer.toString("base64");
|
|
3534
|
-
const ext = path10.extname(imagePath).toLowerCase();
|
|
3535
|
-
let mimeType = "image/png";
|
|
3536
|
-
if (ext === ".jpg" || ext === ".jpeg") {
|
|
3537
|
-
mimeType = "image/jpeg";
|
|
3538
|
-
} else if (ext === ".webp") {
|
|
3539
|
-
mimeType = "image/webp";
|
|
3540
|
-
}
|
|
3541
|
-
return { data: base64, mimeType };
|
|
3542
|
-
}
|
|
3543
3678
|
function getAspectRatioForDevice(deviceType) {
|
|
3544
3679
|
return DEVICE_ASPECT_RATIOS[deviceType];
|
|
3545
3680
|
}
|
|
3546
|
-
async function translateImage(sourcePath, sourceLocale, targetLocale, outputPaths, deviceType, preserveWords) {
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
const preserveInstruction = preserveWords && preserveWords.length > 0 ? `
|
|
3681
|
+
async function translateImage(sourcePath, sourceLocale, targetLocale, outputPaths, deviceType, preserveWords, imageModel = "flash") {
|
|
3682
|
+
const client = createGeminiClient();
|
|
3683
|
+
const sourceLanguage = getLanguageName(sourceLocale);
|
|
3684
|
+
const targetLanguage = getLanguageName(targetLocale);
|
|
3685
|
+
const aspectRatio = getAspectRatioForDevice(deviceType);
|
|
3686
|
+
const { data: imageData, mimeType } = readImageAsBase64(sourcePath);
|
|
3687
|
+
const preserveInstruction = preserveWords && preserveWords.length > 0 ? `
|
|
3554
3688
|
- Do NOT translate these words, keep them exactly as-is: ${preserveWords.join(", ")}` : "";
|
|
3555
|
-
|
|
3689
|
+
const prompt = `This is an app screenshot with text in ${sourceLanguage}.
|
|
3556
3690
|
Please translate ONLY the text/words in this image to ${targetLanguage}.
|
|
3557
3691
|
|
|
3558
3692
|
IMPORTANT INSTRUCTIONS:
|
|
@@ -3562,73 +3696,38 @@ IMPORTANT INSTRUCTIONS:
|
|
|
3562
3696
|
- Do NOT add any new elements or remove existing design elements
|
|
3563
3697
|
- The output should look identical except the text language is ${targetLanguage}
|
|
3564
3698
|
- Preserve all icons, images, and graphical elements exactly as they are${preserveInstruction}`;
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
inlineData: {
|
|
3576
|
-
mimeType,
|
|
3577
|
-
data: imageData
|
|
3578
|
-
}
|
|
3579
|
-
}
|
|
3580
|
-
],
|
|
3581
|
-
config: {
|
|
3582
|
-
responseModalities: ["TEXT", "IMAGE"],
|
|
3583
|
-
imageConfig: {
|
|
3584
|
-
aspectRatio
|
|
3585
|
-
}
|
|
3586
|
-
}
|
|
3699
|
+
try {
|
|
3700
|
+
const generated = await generateImageWithFallback({
|
|
3701
|
+
client,
|
|
3702
|
+
prompt,
|
|
3703
|
+
image: {
|
|
3704
|
+
mimeType,
|
|
3705
|
+
data: imageData
|
|
3706
|
+
},
|
|
3707
|
+
aspectRatio,
|
|
3708
|
+
imageModel
|
|
3587
3709
|
});
|
|
3588
|
-
const
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
};
|
|
3594
|
-
}
|
|
3595
|
-
const parts = candidates[0].content?.parts;
|
|
3596
|
-
if (!parts) {
|
|
3597
|
-
return {
|
|
3598
|
-
success: false,
|
|
3599
|
-
error: "No content parts in response"
|
|
3600
|
-
};
|
|
3601
|
-
}
|
|
3602
|
-
for (const part of parts) {
|
|
3603
|
-
if (part.inlineData?.data) {
|
|
3604
|
-
const imageBuffer = Buffer.from(part.inlineData.data, "base64");
|
|
3605
|
-
for (const outputPath of outputPaths) {
|
|
3606
|
-
const outputDir = path10.dirname(outputPath);
|
|
3607
|
-
if (!fs10.existsSync(outputDir)) {
|
|
3608
|
-
fs10.mkdirSync(outputDir, { recursive: true });
|
|
3609
|
-
}
|
|
3610
|
-
await sharp(imageBuffer).png().toFile(outputPath);
|
|
3611
|
-
}
|
|
3612
|
-
return {
|
|
3613
|
-
success: true,
|
|
3614
|
-
outputPath: outputPaths[0]
|
|
3615
|
-
// Return primary path
|
|
3616
|
-
};
|
|
3710
|
+
const imageBuffer = Buffer.from(generated.imageBase64, "base64");
|
|
3711
|
+
for (const outputPath of outputPaths) {
|
|
3712
|
+
const outputDir = path11.dirname(outputPath);
|
|
3713
|
+
if (!fs11.existsSync(outputDir)) {
|
|
3714
|
+
fs11.mkdirSync(outputDir, { recursive: true });
|
|
3617
3715
|
}
|
|
3716
|
+
await sharp(imageBuffer).png().toFile(outputPath);
|
|
3618
3717
|
}
|
|
3619
3718
|
return {
|
|
3620
|
-
success:
|
|
3621
|
-
|
|
3719
|
+
success: true,
|
|
3720
|
+
outputPath: outputPaths[0]
|
|
3721
|
+
// Return primary path
|
|
3622
3722
|
};
|
|
3623
3723
|
} catch (error) {
|
|
3624
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3625
3724
|
return {
|
|
3626
3725
|
success: false,
|
|
3627
|
-
error: message
|
|
3726
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3628
3727
|
};
|
|
3629
3728
|
}
|
|
3630
3729
|
}
|
|
3631
|
-
async function translateImagesWithProgress(translations, onProgress, preserveWords) {
|
|
3730
|
+
async function translateImagesWithProgress(translations, onProgress, preserveWords, imageModel = "flash") {
|
|
3632
3731
|
let successful = 0;
|
|
3633
3732
|
let failed = 0;
|
|
3634
3733
|
const errors = [];
|
|
@@ -3652,7 +3751,8 @@ async function translateImagesWithProgress(translations, onProgress, preserveWor
|
|
|
3652
3751
|
translation.targetLocale,
|
|
3653
3752
|
translation.outputPaths,
|
|
3654
3753
|
translation.deviceType,
|
|
3655
|
-
preserveWords
|
|
3754
|
+
preserveWords,
|
|
3755
|
+
imageModel
|
|
3656
3756
|
);
|
|
3657
3757
|
if (result.success) {
|
|
3658
3758
|
successful++;
|
|
@@ -3671,6 +3771,9 @@ async function translateImagesWithProgress(translations, onProgress, preserveWor
|
|
|
3671
3771
|
}
|
|
3672
3772
|
return { successful, failed, errors };
|
|
3673
3773
|
}
|
|
3774
|
+
function getImageModelLabel(imageModel) {
|
|
3775
|
+
return `${imageModel} (${GEMINI_IMAGE_MODEL_PRESETS[imageModel]})`;
|
|
3776
|
+
}
|
|
3674
3777
|
|
|
3675
3778
|
// src/tools/screenshots/utils/locale-mapping.constants.ts
|
|
3676
3779
|
var GEMINI_LOCALE_GROUPS = {
|
|
@@ -3773,6 +3876,9 @@ var translateScreenshotsInputSchema = z7.object({
|
|
|
3773
3876
|
),
|
|
3774
3877
|
preserveWords: z7.array(z7.string()).optional().describe(
|
|
3775
3878
|
'Words to keep untranslated (e.g., brand names, product names). Example: ["Pabal", "Pro", "AI"]'
|
|
3879
|
+
),
|
|
3880
|
+
imageModel: z7.enum(GEMINI_IMAGE_MODEL_VALUES).optional().default("flash").describe(
|
|
3881
|
+
"Gemini image model preference. 'flash' (default) is faster/cheaper, 'pro' prioritizes quality."
|
|
3776
3882
|
)
|
|
3777
3883
|
});
|
|
3778
3884
|
var jsonSchema7 = zodToJsonSchema7(translateScreenshotsInputSchema, {
|
|
@@ -3798,6 +3904,10 @@ Use \`resize-screenshots\` after this tool to resize images to final dimensions.
|
|
|
3798
3904
|
- Screenshots must be in: public/products/{slug}/screenshots/{locale}/phone/ and /tablet/
|
|
3799
3905
|
- Locale files must exist in: public/products/{slug}/locales/
|
|
3800
3906
|
|
|
3907
|
+
**Model Selection:**
|
|
3908
|
+
- \`imageModel: "flash"\` (default) for speed/cost
|
|
3909
|
+
- \`imageModel: "pro"\` for higher instruction fidelity
|
|
3910
|
+
|
|
3801
3911
|
**Example output structure:**
|
|
3802
3912
|
\`\`\`
|
|
3803
3913
|
public/products/my-app/screenshots/
|
|
@@ -3873,14 +3983,14 @@ function buildTranslationTasks(slug, screenshots, primaryLocale, targetLocales,
|
|
|
3873
3983
|
for (const screenshot of screenshots) {
|
|
3874
3984
|
const outputPaths = [];
|
|
3875
3985
|
for (const locale of outputLocales) {
|
|
3876
|
-
const outputPath =
|
|
3986
|
+
const outputPath = path12.join(
|
|
3877
3987
|
screenshotsDir,
|
|
3878
3988
|
locale,
|
|
3879
3989
|
screenshot.type,
|
|
3880
3990
|
"raw",
|
|
3881
3991
|
screenshot.filename
|
|
3882
3992
|
);
|
|
3883
|
-
if (!skipExisting || !
|
|
3993
|
+
if (!skipExisting || !fs12.existsSync(outputPath)) {
|
|
3884
3994
|
outputPaths.push(outputPath);
|
|
3885
3995
|
}
|
|
3886
3996
|
}
|
|
@@ -3907,7 +4017,8 @@ async function handleTranslateScreenshots(input) {
|
|
|
3907
4017
|
dryRun = false,
|
|
3908
4018
|
skipExisting = true,
|
|
3909
4019
|
screenshotNumbers,
|
|
3910
|
-
preserveWords
|
|
4020
|
+
preserveWords,
|
|
4021
|
+
imageModel = "flash"
|
|
3911
4022
|
} = input;
|
|
3912
4023
|
const results = [];
|
|
3913
4024
|
let appInfo;
|
|
@@ -3960,6 +4071,7 @@ async function handleTranslateScreenshots(input) {
|
|
|
3960
4071
|
};
|
|
3961
4072
|
}
|
|
3962
4073
|
results.push(`\u{1F3AF} Target locales to translate: ${targetLocales.join(", ")}`);
|
|
4074
|
+
results.push(`\u{1F9E0} Image model: ${getImageModelLabel(imageModel)}`);
|
|
3963
4075
|
if (groupedLocales.length > 0) {
|
|
3964
4076
|
results.push(
|
|
3965
4077
|
`\u{1F4CB} Grouped locales (saved together): ${groupedLocales.join(", ")}`
|
|
@@ -4087,9 +4199,9 @@ ${screenshotsDir2}/${primaryLocale}/tablet/1.png, 2.png, ...`
|
|
|
4087
4199
|
}
|
|
4088
4200
|
for (const task of tasks) {
|
|
4089
4201
|
for (const outputPath of task.outputPaths) {
|
|
4090
|
-
const outputDir =
|
|
4091
|
-
if (!
|
|
4092
|
-
|
|
4202
|
+
const outputDir = path12.dirname(outputPath);
|
|
4203
|
+
if (!fs12.existsSync(outputDir)) {
|
|
4204
|
+
fs12.mkdirSync(outputDir, { recursive: true });
|
|
4093
4205
|
}
|
|
4094
4206
|
}
|
|
4095
4207
|
}
|
|
@@ -4111,7 +4223,8 @@ ${screenshotsDir2}/${primaryLocale}/tablet/1.png, 2.png, ...`
|
|
|
4111
4223
|
);
|
|
4112
4224
|
}
|
|
4113
4225
|
},
|
|
4114
|
-
preserveWords
|
|
4226
|
+
preserveWords,
|
|
4227
|
+
imageModel
|
|
4115
4228
|
);
|
|
4116
4229
|
results.push(`
|
|
4117
4230
|
\u{1F4CA} Translation Results:`);
|
|
@@ -4121,7 +4234,7 @@ ${screenshotsDir2}/${primaryLocale}/tablet/1.png, 2.png, ...`
|
|
|
4121
4234
|
results.push(`
|
|
4122
4235
|
\u26A0\uFE0F Errors:`);
|
|
4123
4236
|
for (const err of translationResult.errors.slice(0, 5)) {
|
|
4124
|
-
results.push(` - ${
|
|
4237
|
+
results.push(` - ${path12.basename(err.path)}: ${err.error}`);
|
|
4125
4238
|
}
|
|
4126
4239
|
if (translationResult.errors.length > 5) {
|
|
4127
4240
|
results.push(
|
|
@@ -4149,12 +4262,12 @@ ${screenshotsDir2}/${primaryLocale}/tablet/1.png, 2.png, ...`
|
|
|
4149
4262
|
// src/tools/screenshots/resize-screenshots.ts
|
|
4150
4263
|
import { z as z8 } from "zod";
|
|
4151
4264
|
import { zodToJsonSchema as zodToJsonSchema8 } from "zod-to-json-schema";
|
|
4152
|
-
import
|
|
4153
|
-
import
|
|
4265
|
+
import fs14 from "fs";
|
|
4266
|
+
import path13 from "path";
|
|
4154
4267
|
|
|
4155
4268
|
// src/tools/screenshots/utils/image-resizer.util.ts
|
|
4156
4269
|
import sharp2 from "sharp";
|
|
4157
|
-
import
|
|
4270
|
+
import fs13 from "fs";
|
|
4158
4271
|
var SCREENSHOT_DIMENSIONS = {
|
|
4159
4272
|
phone: { width: 1242, height: 2688 },
|
|
4160
4273
|
tablet: { width: 2048, height: 2732 }
|
|
@@ -4253,7 +4366,7 @@ async function resizeImage(inputPath, outputPath, targetDimensions, bgColor) {
|
|
|
4253
4366
|
kernel: "lanczos3"
|
|
4254
4367
|
// High-quality downscaling algorithm
|
|
4255
4368
|
}).flatten({ background: backgroundColor }).png().toFile(outputPath + ".tmp");
|
|
4256
|
-
|
|
4369
|
+
fs13.renameSync(outputPath + ".tmp", outputPath);
|
|
4257
4370
|
}
|
|
4258
4371
|
|
|
4259
4372
|
// src/tools/screenshots/resize-screenshots.ts
|
|
@@ -4375,13 +4488,13 @@ function buildResizeTasks(slug, sourceScreenshots, rawLocales, deviceTypes, scre
|
|
|
4375
4488
|
if (!sourceReferencePath) {
|
|
4376
4489
|
continue;
|
|
4377
4490
|
}
|
|
4378
|
-
const outputPath =
|
|
4491
|
+
const outputPath = path13.join(
|
|
4379
4492
|
screenshotsDir,
|
|
4380
4493
|
locale,
|
|
4381
4494
|
screenshot.type,
|
|
4382
4495
|
screenshot.filename
|
|
4383
4496
|
);
|
|
4384
|
-
if (skipExisting &&
|
|
4497
|
+
if (skipExisting && fs14.existsSync(outputPath)) {
|
|
4385
4498
|
continue;
|
|
4386
4499
|
}
|
|
4387
4500
|
tasks.push({
|
|
@@ -4413,7 +4526,7 @@ async function batchResizeFromRaw(tasks, bgColor, onProgress) {
|
|
|
4413
4526
|
};
|
|
4414
4527
|
onProgress?.(progress);
|
|
4415
4528
|
try {
|
|
4416
|
-
if (!
|
|
4529
|
+
if (!fs14.existsSync(task.rawPath)) {
|
|
4417
4530
|
progress.status = "skipped";
|
|
4418
4531
|
onProgress?.(progress);
|
|
4419
4532
|
skippedCount++;
|
|
@@ -4421,9 +4534,9 @@ async function batchResizeFromRaw(tasks, bgColor, onProgress) {
|
|
|
4421
4534
|
}
|
|
4422
4535
|
const targetDimensions = SCREENSHOT_DIMENSIONS[task.deviceType];
|
|
4423
4536
|
const rawDimensions = await getImageDimensions(task.rawPath);
|
|
4424
|
-
const outputDir =
|
|
4425
|
-
if (!
|
|
4426
|
-
|
|
4537
|
+
const outputDir = path13.dirname(task.outputPath);
|
|
4538
|
+
if (!fs14.existsSync(outputDir)) {
|
|
4539
|
+
fs14.mkdirSync(outputDir, { recursive: true });
|
|
4427
4540
|
}
|
|
4428
4541
|
await resizeImage(task.rawPath, task.outputPath, targetDimensions, bgColor);
|
|
4429
4542
|
progress.status = "completed";
|
|
@@ -4650,7 +4763,7 @@ Available locales with raw/: ${allRawLocales.join(", ")}`
|
|
|
4650
4763
|
results.push(`
|
|
4651
4764
|
\u26A0\uFE0F Errors:`);
|
|
4652
4765
|
for (const err of resizeResult.errors.slice(0, 5)) {
|
|
4653
|
-
results.push(` - ${
|
|
4766
|
+
results.push(` - ${path13.basename(err.path)}: ${err.error}`);
|
|
4654
4767
|
}
|
|
4655
4768
|
if (resizeResult.errors.length > 5) {
|
|
4656
4769
|
results.push(` ... and ${resizeResult.errors.length - 5} more errors`);
|
|
@@ -4674,10 +4787,9 @@ Available locales with raw/: ${allRawLocales.join(", ")}`
|
|
|
4674
4787
|
// src/tools/screenshots/phone-to-tablet.ts
|
|
4675
4788
|
import { z as z9 } from "zod";
|
|
4676
4789
|
import { zodToJsonSchema as zodToJsonSchema9 } from "zod-to-json-schema";
|
|
4677
|
-
import
|
|
4678
|
-
import
|
|
4790
|
+
import fs15 from "fs";
|
|
4791
|
+
import path14 from "path";
|
|
4679
4792
|
import sharp3 from "sharp";
|
|
4680
|
-
import { GoogleGenAI as GoogleGenAI2 } from "@google/genai";
|
|
4681
4793
|
var TOOL_NAME5 = "phone-to-tablet";
|
|
4682
4794
|
var TABLET_ASPECT_RATIO = "3:4";
|
|
4683
4795
|
var phoneToTabletInputSchema = z9.object({
|
|
@@ -4698,6 +4810,9 @@ var phoneToTabletInputSchema = z9.object({
|
|
|
4698
4810
|
),
|
|
4699
4811
|
preserveWords: z9.array(z9.string()).optional().describe(
|
|
4700
4812
|
'Words to keep exactly as-is in the generated image (e.g., brand names). Example: ["Pabal", "Pro", "AI"]'
|
|
4813
|
+
),
|
|
4814
|
+
imageModel: z9.enum(GEMINI_IMAGE_MODEL_VALUES).optional().default("flash").describe(
|
|
4815
|
+
"Gemini image model preference. 'flash' (default) is faster/cheaper, 'pro' prioritizes quality."
|
|
4701
4816
|
)
|
|
4702
4817
|
});
|
|
4703
4818
|
var jsonSchema9 = zodToJsonSchema9(phoneToTabletInputSchema, {
|
|
@@ -4728,6 +4843,10 @@ Run \`resize-screenshots --deviceTypes tablet\` after this tool to resize images
|
|
|
4728
4843
|
- Phone screenshots must exist in: public/products/{slug}/screenshots/{locale}/phone/
|
|
4729
4844
|
- Locale files must exist in: public/products/{slug}/locales/
|
|
4730
4845
|
|
|
4846
|
+
**Model Selection:**
|
|
4847
|
+
- \`imageModel: "flash"\` (default) for speed/cost
|
|
4848
|
+
- \`imageModel: "pro"\` for higher instruction fidelity
|
|
4849
|
+
|
|
4731
4850
|
**Example output structure:**
|
|
4732
4851
|
\`\`\`
|
|
4733
4852
|
public/products/my-app/screenshots/
|
|
@@ -4779,14 +4898,14 @@ function buildConversionTasks(slug, phoneScreenshots, locale, screenshotNumbers,
|
|
|
4779
4898
|
});
|
|
4780
4899
|
}
|
|
4781
4900
|
for (const screenshot of filteredScreenshots) {
|
|
4782
|
-
const tabletRawPath =
|
|
4901
|
+
const tabletRawPath = path14.join(
|
|
4783
4902
|
screenshotsDir,
|
|
4784
4903
|
locale,
|
|
4785
4904
|
"tablet",
|
|
4786
4905
|
"raw",
|
|
4787
4906
|
screenshot.filename
|
|
4788
4907
|
);
|
|
4789
|
-
if (skipExisting &&
|
|
4908
|
+
if (skipExisting && fs15.existsSync(tabletRawPath)) {
|
|
4790
4909
|
continue;
|
|
4791
4910
|
}
|
|
4792
4911
|
tasks.push({
|
|
@@ -4798,29 +4917,12 @@ function buildConversionTasks(slug, phoneScreenshots, locale, screenshotNumbers,
|
|
|
4798
4917
|
}
|
|
4799
4918
|
return tasks;
|
|
4800
4919
|
}
|
|
4801
|
-
function
|
|
4802
|
-
const
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
function readImageAsBase642(imagePath) {
|
|
4806
|
-
const buffer = fs14.readFileSync(imagePath);
|
|
4807
|
-
const base64 = buffer.toString("base64");
|
|
4808
|
-
const ext = path13.extname(imagePath).toLowerCase();
|
|
4809
|
-
let mimeType = "image/png";
|
|
4810
|
-
if (ext === ".jpg" || ext === ".jpeg") {
|
|
4811
|
-
mimeType = "image/jpeg";
|
|
4812
|
-
} else if (ext === ".webp") {
|
|
4813
|
-
mimeType = "image/webp";
|
|
4814
|
-
}
|
|
4815
|
-
return { data: base64, mimeType };
|
|
4816
|
-
}
|
|
4817
|
-
async function convertPhoneToTablet(phonePath, tabletRawPath, preserveWords) {
|
|
4818
|
-
try {
|
|
4819
|
-
const client = getGeminiClient2();
|
|
4820
|
-
const { data: imageData, mimeType } = readImageAsBase642(phonePath);
|
|
4821
|
-
const preserveInstruction = preserveWords && preserveWords.length > 0 ? `
|
|
4920
|
+
async function convertPhoneToTablet(phonePath, tabletRawPath, preserveWords, imageModel = "flash") {
|
|
4921
|
+
const client = createGeminiClient();
|
|
4922
|
+
const { data: imageData, mimeType } = readImageAsBase64(phonePath);
|
|
4923
|
+
const preserveInstruction = preserveWords && preserveWords.length > 0 ? `
|
|
4822
4924
|
- Do NOT change these words, keep them exactly as-is: ${preserveWords.join(", ")}` : "";
|
|
4823
|
-
|
|
4925
|
+
const prompt = `This is a phone app screenshot. Please recreate this screenshot as a TABLET version.
|
|
4824
4926
|
|
|
4825
4927
|
IMPORTANT INSTRUCTIONS:
|
|
4826
4928
|
- Convert this phone UI layout to a tablet-friendly WIDER layout
|
|
@@ -4834,67 +4936,32 @@ IMPORTANT INSTRUCTIONS:
|
|
|
4834
4936
|
- Keep the same app functionality visible, just optimized for tablet${preserveInstruction}
|
|
4835
4937
|
|
|
4836
4938
|
Generate a new tablet screenshot that represents the same app screen but optimized for tablet display.`;
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
inlineData: {
|
|
4848
|
-
mimeType,
|
|
4849
|
-
data: imageData
|
|
4850
|
-
}
|
|
4851
|
-
}
|
|
4852
|
-
],
|
|
4853
|
-
config: {
|
|
4854
|
-
responseModalities: ["TEXT", "IMAGE"],
|
|
4855
|
-
imageConfig: {
|
|
4856
|
-
aspectRatio: TABLET_ASPECT_RATIO
|
|
4857
|
-
}
|
|
4858
|
-
}
|
|
4939
|
+
try {
|
|
4940
|
+
const generated = await generateImageWithFallback({
|
|
4941
|
+
client,
|
|
4942
|
+
prompt,
|
|
4943
|
+
image: {
|
|
4944
|
+
mimeType,
|
|
4945
|
+
data: imageData
|
|
4946
|
+
},
|
|
4947
|
+
aspectRatio: TABLET_ASPECT_RATIO,
|
|
4948
|
+
imageModel
|
|
4859
4949
|
});
|
|
4860
|
-
const
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
error: "No response from Gemini API"
|
|
4865
|
-
};
|
|
4866
|
-
}
|
|
4867
|
-
const parts = candidates[0].content?.parts;
|
|
4868
|
-
if (!parts) {
|
|
4869
|
-
return {
|
|
4870
|
-
success: false,
|
|
4871
|
-
error: "No content parts in response"
|
|
4872
|
-
};
|
|
4873
|
-
}
|
|
4874
|
-
for (const part of parts) {
|
|
4875
|
-
if (part.inlineData?.data) {
|
|
4876
|
-
const imageBuffer = Buffer.from(part.inlineData.data, "base64");
|
|
4877
|
-
const outputDir = path13.dirname(tabletRawPath);
|
|
4878
|
-
if (!fs14.existsSync(outputDir)) {
|
|
4879
|
-
fs14.mkdirSync(outputDir, { recursive: true });
|
|
4880
|
-
}
|
|
4881
|
-
await sharp3(imageBuffer).png().toFile(tabletRawPath);
|
|
4882
|
-
return { success: true };
|
|
4883
|
-
}
|
|
4950
|
+
const imageBuffer = Buffer.from(generated.imageBase64, "base64");
|
|
4951
|
+
const outputDir = path14.dirname(tabletRawPath);
|
|
4952
|
+
if (!fs15.existsSync(outputDir)) {
|
|
4953
|
+
fs15.mkdirSync(outputDir, { recursive: true });
|
|
4884
4954
|
}
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
error: "No image data in Gemini response"
|
|
4888
|
-
};
|
|
4955
|
+
await sharp3(imageBuffer).png().toFile(tabletRawPath);
|
|
4956
|
+
return { success: true };
|
|
4889
4957
|
} catch (error) {
|
|
4890
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
4891
4958
|
return {
|
|
4892
4959
|
success: false,
|
|
4893
|
-
error: message
|
|
4960
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4894
4961
|
};
|
|
4895
4962
|
}
|
|
4896
4963
|
}
|
|
4897
|
-
async function convertWithProgress(tasks, onProgress, preserveWords) {
|
|
4964
|
+
async function convertWithProgress(tasks, onProgress, preserveWords, imageModel = "flash") {
|
|
4898
4965
|
let successful = 0;
|
|
4899
4966
|
let failed = 0;
|
|
4900
4967
|
const errors = [];
|
|
@@ -4913,7 +4980,8 @@ async function convertWithProgress(tasks, onProgress, preserveWords) {
|
|
|
4913
4980
|
const result = await convertPhoneToTablet(
|
|
4914
4981
|
task.phonePath,
|
|
4915
4982
|
task.tabletRawPath,
|
|
4916
|
-
preserveWords
|
|
4983
|
+
preserveWords,
|
|
4984
|
+
imageModel
|
|
4917
4985
|
);
|
|
4918
4986
|
if (result.success) {
|
|
4919
4987
|
successful++;
|
|
@@ -4939,7 +5007,8 @@ async function handlePhoneToTablet(input) {
|
|
|
4939
5007
|
screenshotNumbers,
|
|
4940
5008
|
dryRun = false,
|
|
4941
5009
|
skipExisting = true,
|
|
4942
|
-
preserveWords
|
|
5010
|
+
preserveWords,
|
|
5011
|
+
imageModel = "flash"
|
|
4943
5012
|
} = input;
|
|
4944
5013
|
const results = [];
|
|
4945
5014
|
let appInfo;
|
|
@@ -4989,6 +5058,9 @@ async function handlePhoneToTablet(input) {
|
|
|
4989
5058
|
}
|
|
4990
5059
|
}
|
|
4991
5060
|
results.push(`\u{1F3AF} Locales to process: ${localesToProcess.join(", ")}`);
|
|
5061
|
+
results.push(
|
|
5062
|
+
`\u{1F9E0} Image model: ${imageModel} (${GEMINI_IMAGE_MODEL_PRESETS[imageModel]})`
|
|
5063
|
+
);
|
|
4992
5064
|
const allTasks = [];
|
|
4993
5065
|
for (const locale of localesToProcess) {
|
|
4994
5066
|
const phoneScreenshots = scanLocaleScreenshots(appInfo.slug, locale).filter(
|
|
@@ -5086,7 +5158,8 @@ Expected phone screenshots in: ${screenshotsDir2}/{locale}/phone/`
|
|
|
5086
5158
|
);
|
|
5087
5159
|
}
|
|
5088
5160
|
},
|
|
5089
|
-
preserveWords
|
|
5161
|
+
preserveWords,
|
|
5162
|
+
imageModel
|
|
5090
5163
|
);
|
|
5091
5164
|
results.push(`
|
|
5092
5165
|
\u{1F4CA} Conversion Results:`);
|
|
@@ -5096,7 +5169,7 @@ Expected phone screenshots in: ${screenshotsDir2}/{locale}/phone/`
|
|
|
5096
5169
|
results.push(`
|
|
5097
5170
|
\u26A0\uFE0F Errors:`);
|
|
5098
5171
|
for (const err of conversionResult.errors.slice(0, 5)) {
|
|
5099
|
-
results.push(` - ${
|
|
5172
|
+
results.push(` - ${path14.basename(err.path)}: ${err.error}`);
|
|
5100
5173
|
}
|
|
5101
5174
|
if (conversionResult.errors.length > 5) {
|
|
5102
5175
|
results.push(
|
|
@@ -5126,12 +5199,12 @@ Expected phone screenshots in: ${screenshotsDir2}/{locale}/phone/`
|
|
|
5126
5199
|
// src/tools/app-icon/generate-app-icons.ts
|
|
5127
5200
|
import { z as z10 } from "zod";
|
|
5128
5201
|
import { zodToJsonSchema as zodToJsonSchema10 } from "zod-to-json-schema";
|
|
5129
|
-
import
|
|
5130
|
-
import
|
|
5202
|
+
import fs16 from "fs";
|
|
5203
|
+
import path16 from "path";
|
|
5131
5204
|
import sharp5 from "sharp";
|
|
5132
5205
|
|
|
5133
5206
|
// src/tools/app-icon/utils/icon-specs.util.ts
|
|
5134
|
-
import
|
|
5207
|
+
import path15 from "path";
|
|
5135
5208
|
var ICON_FILENAMES = {
|
|
5136
5209
|
BASE: "icon.png",
|
|
5137
5210
|
IOS_LIGHT: "ios-light.png",
|
|
@@ -5172,14 +5245,14 @@ var ALL_ICON_TYPES = Object.keys(
|
|
|
5172
5245
|
);
|
|
5173
5246
|
function getIconsDir(slug, styleFolder) {
|
|
5174
5247
|
const productsDir = getProductsDir();
|
|
5175
|
-
const baseDir =
|
|
5176
|
-
return styleFolder ?
|
|
5248
|
+
const baseDir = path15.join(productsDir, slug, "icons");
|
|
5249
|
+
return styleFolder ? path15.join(baseDir, styleFolder) : baseDir;
|
|
5177
5250
|
}
|
|
5178
5251
|
function getBaseIconPath(slug, styleFolder) {
|
|
5179
|
-
return
|
|
5252
|
+
return path15.join(getIconsDir(slug, styleFolder), ICON_FILENAMES.BASE);
|
|
5180
5253
|
}
|
|
5181
5254
|
function getIconOutputPath(slug, iconType, styleFolder) {
|
|
5182
|
-
return
|
|
5255
|
+
return path15.join(getIconsDir(slug, styleFolder), ICON_SPECS[iconType].filename);
|
|
5183
5256
|
}
|
|
5184
5257
|
|
|
5185
5258
|
// src/tools/app-icon/utils/icon-resizer.util.ts
|
|
@@ -5489,11 +5562,11 @@ function validateApp4(appName) {
|
|
|
5489
5562
|
);
|
|
5490
5563
|
}
|
|
5491
5564
|
const productsDir = getProductsDir();
|
|
5492
|
-
const configPath =
|
|
5565
|
+
const configPath = path16.join(productsDir, app.slug, "config.json");
|
|
5493
5566
|
let config;
|
|
5494
|
-
if (
|
|
5567
|
+
if (fs16.existsSync(configPath)) {
|
|
5495
5568
|
try {
|
|
5496
|
-
const configData =
|
|
5569
|
+
const configData = fs16.readFileSync(configPath, "utf-8");
|
|
5497
5570
|
config = JSON.parse(configData);
|
|
5498
5571
|
} catch (error) {
|
|
5499
5572
|
console.warn(
|
|
@@ -5512,7 +5585,7 @@ function buildGenerationTasks(slug, iconTypes, skipExisting, styleFolder) {
|
|
|
5512
5585
|
const tasks = [];
|
|
5513
5586
|
const baseIconPath = getBaseIconPath(slug, styleFolder);
|
|
5514
5587
|
const iconsDir = getIconsDir(slug, styleFolder);
|
|
5515
|
-
if (!
|
|
5588
|
+
if (!fs16.existsSync(baseIconPath)) {
|
|
5516
5589
|
throw new Error(
|
|
5517
5590
|
`Base icon not found: ${baseIconPath}
|
|
5518
5591
|
|
|
@@ -5521,7 +5594,7 @@ Please place your base icon at this location first.`
|
|
|
5521
5594
|
}
|
|
5522
5595
|
for (const iconType of iconTypes) {
|
|
5523
5596
|
const outputPath = getIconOutputPath(slug, iconType, styleFolder);
|
|
5524
|
-
if (skipExisting &&
|
|
5597
|
+
if (skipExisting && fs16.existsSync(outputPath)) {
|
|
5525
5598
|
continue;
|
|
5526
5599
|
}
|
|
5527
5600
|
tasks.push({
|
|
@@ -5547,9 +5620,9 @@ async function generateIcons(tasks, backgroundColor, logoAlignment, onProgress)
|
|
|
5547
5620
|
};
|
|
5548
5621
|
onProgress?.(progress);
|
|
5549
5622
|
try {
|
|
5550
|
-
const outputDir =
|
|
5551
|
-
if (!
|
|
5552
|
-
|
|
5623
|
+
const outputDir = path16.dirname(task.outputPath);
|
|
5624
|
+
if (!fs16.existsSync(outputDir)) {
|
|
5625
|
+
fs16.mkdirSync(outputDir, { recursive: true });
|
|
5553
5626
|
}
|
|
5554
5627
|
if (task.iconType === "android-notification-icon") {
|
|
5555
5628
|
const whiteMask = await convertToWhiteMask(task.inputPath);
|
|
@@ -5753,24 +5826,8 @@ import path17 from "path";
|
|
|
5753
5826
|
import sharp6 from "sharp";
|
|
5754
5827
|
|
|
5755
5828
|
// src/tools/app-icon/utils/gemini.util.ts
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
import { GoogleGenAI as GoogleGenAI3 } from "@google/genai";
|
|
5759
|
-
function getGeminiClient3() {
|
|
5760
|
-
const apiKey = getGeminiApiKey();
|
|
5761
|
-
return new GoogleGenAI3({ apiKey });
|
|
5762
|
-
}
|
|
5763
|
-
function readImageAsBase643(imagePath) {
|
|
5764
|
-
const buffer = fs16.readFileSync(imagePath);
|
|
5765
|
-
const base64 = buffer.toString("base64");
|
|
5766
|
-
const ext = path16.extname(imagePath).toLowerCase();
|
|
5767
|
-
let mimeType = "image/png";
|
|
5768
|
-
if (ext === ".jpg" || ext === ".jpeg") {
|
|
5769
|
-
mimeType = "image/jpeg";
|
|
5770
|
-
} else if (ext === ".webp") {
|
|
5771
|
-
mimeType = "image/webp";
|
|
5772
|
-
}
|
|
5773
|
-
return { data: base64, mimeType };
|
|
5829
|
+
function getGeminiClient() {
|
|
5830
|
+
return createGeminiClient();
|
|
5774
5831
|
}
|
|
5775
5832
|
|
|
5776
5833
|
// src/tools/app-icon/stylize-app-icon.ts
|
|
@@ -5788,6 +5845,9 @@ var stylizeAppIconInputSchema = z11.object({
|
|
|
5788
5845
|
preserveShape: z11.boolean().optional().default(true).describe(
|
|
5789
5846
|
"Preserve the original icon shape and structure (default: true). When true, only applies style elements without changing the core design."
|
|
5790
5847
|
),
|
|
5848
|
+
imageModel: z11.enum(GEMINI_IMAGE_MODEL_VALUES).optional().default("flash").describe(
|
|
5849
|
+
"Gemini image model preference. 'flash' (default) is faster/cheaper, 'pro' prioritizes quality."
|
|
5850
|
+
),
|
|
5791
5851
|
dryRun: z11.boolean().optional().default(false).describe(
|
|
5792
5852
|
"Preview mode - shows what would be generated without actually calling API or saving files"
|
|
5793
5853
|
)
|
|
@@ -5816,6 +5876,10 @@ var stylizeAppIconTool = {
|
|
|
5816
5876
|
- GEMINI_API_KEY or GOOGLE_API_KEY environment variable
|
|
5817
5877
|
- Base icon exists at {slug}/icons/icon.png
|
|
5818
5878
|
|
|
5879
|
+
**Model Selection:**
|
|
5880
|
+
- \`imageModel: "flash"\` (default) for speed/cost
|
|
5881
|
+
- \`imageModel: "pro"\` for higher instruction fidelity
|
|
5882
|
+
|
|
5819
5883
|
**Example Flow:**
|
|
5820
5884
|
\`\`\`
|
|
5821
5885
|
Step 1: Stylize icon
|
|
@@ -5850,10 +5914,10 @@ function validateApp5(appName) {
|
|
|
5850
5914
|
name: app.name || app.slug
|
|
5851
5915
|
};
|
|
5852
5916
|
}
|
|
5853
|
-
async function stylizeIconWithAI(inputPath, outputPath, stylePrompt, preserveShape) {
|
|
5917
|
+
async function stylizeIconWithAI(inputPath, outputPath, stylePrompt, preserveShape, imageModel = "flash") {
|
|
5854
5918
|
try {
|
|
5855
|
-
const client =
|
|
5856
|
-
const { data: imageData, mimeType } =
|
|
5919
|
+
const client = getGeminiClient();
|
|
5920
|
+
const { data: imageData, mimeType } = readImageAsBase64(inputPath);
|
|
5857
5921
|
const shapeInstruction = preserveShape ? "IMPORTANT: Preserve the original icon's shape, structure, and core design elements. Only add style-specific decorations and color adjustments." : "You can modify the icon structure as needed to achieve the style.";
|
|
5858
5922
|
const fullPrompt = `You are an expert app icon designer. Transform this app icon with the following style:
|
|
5859
5923
|
|
|
@@ -5870,56 +5934,23 @@ Requirements:
|
|
|
5870
5934
|
6. Ensure the result is visually appealing and brand-appropriate
|
|
5871
5935
|
|
|
5872
5936
|
Generate the stylized icon with transparent background now.`;
|
|
5873
|
-
const
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
{ text: fullPrompt },
|
|
5882
|
-
{
|
|
5883
|
-
inlineData: {
|
|
5884
|
-
mimeType,
|
|
5885
|
-
data: imageData
|
|
5886
|
-
}
|
|
5887
|
-
}
|
|
5888
|
-
],
|
|
5889
|
-
config: {
|
|
5890
|
-
responseModalities: ["TEXT", "IMAGE"]
|
|
5891
|
-
}
|
|
5937
|
+
const generated = await generateImageWithFallback({
|
|
5938
|
+
client,
|
|
5939
|
+
prompt: fullPrompt,
|
|
5940
|
+
image: {
|
|
5941
|
+
mimeType,
|
|
5942
|
+
data: imageData
|
|
5943
|
+
},
|
|
5944
|
+
imageModel
|
|
5892
5945
|
});
|
|
5893
|
-
const
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
if (!parts) {
|
|
5902
|
-
return {
|
|
5903
|
-
success: false,
|
|
5904
|
-
error: "No content parts in response"
|
|
5905
|
-
};
|
|
5906
|
-
}
|
|
5907
|
-
for (const part of parts) {
|
|
5908
|
-
if (part.inlineData?.data) {
|
|
5909
|
-
const imageBuffer = Buffer.from(part.inlineData.data, "base64");
|
|
5910
|
-
const outputDir = path17.dirname(outputPath);
|
|
5911
|
-
if (!fs17.existsSync(outputDir)) {
|
|
5912
|
-
fs17.mkdirSync(outputDir, { recursive: true });
|
|
5913
|
-
}
|
|
5914
|
-
const processedImage = await sharp6(imageBuffer).ensureAlpha().png().toBuffer();
|
|
5915
|
-
await sharp6(processedImage).toFile(outputPath);
|
|
5916
|
-
return { success: true };
|
|
5917
|
-
}
|
|
5918
|
-
}
|
|
5919
|
-
return {
|
|
5920
|
-
success: false,
|
|
5921
|
-
error: "No image data in Gemini response"
|
|
5922
|
-
};
|
|
5946
|
+
const imageBuffer = Buffer.from(generated.imageBase64, "base64");
|
|
5947
|
+
const outputDir = path17.dirname(outputPath);
|
|
5948
|
+
if (!fs17.existsSync(outputDir)) {
|
|
5949
|
+
fs17.mkdirSync(outputDir, { recursive: true });
|
|
5950
|
+
}
|
|
5951
|
+
const processedImage = await sharp6(imageBuffer).ensureAlpha().png().toBuffer();
|
|
5952
|
+
await sharp6(processedImage).toFile(outputPath);
|
|
5953
|
+
return { success: true };
|
|
5923
5954
|
} catch (error) {
|
|
5924
5955
|
const message = error instanceof Error ? error.message : String(error);
|
|
5925
5956
|
return {
|
|
@@ -5934,6 +5965,7 @@ async function handleStylizeAppIcon(input) {
|
|
|
5934
5965
|
styleFolder,
|
|
5935
5966
|
stylePrompt,
|
|
5936
5967
|
preserveShape = true,
|
|
5968
|
+
imageModel = "flash",
|
|
5937
5969
|
dryRun = false
|
|
5938
5970
|
} = input;
|
|
5939
5971
|
const results = [];
|
|
@@ -5975,6 +6007,9 @@ Please place your base icon at this location first.`
|
|
|
5975
6007
|
results.push(
|
|
5976
6008
|
`\u{1F527} Shape preservation: ${preserveShape ? "enabled" : "disabled"}`
|
|
5977
6009
|
);
|
|
6010
|
+
results.push(
|
|
6011
|
+
`\u{1F9E0} Image model: ${imageModel} (${GEMINI_IMAGE_MODEL_PRESETS[imageModel]})`
|
|
6012
|
+
);
|
|
5978
6013
|
if (dryRun) {
|
|
5979
6014
|
results.push(`
|
|
5980
6015
|
\u{1F50D} DRY RUN - No actual generation will be performed`);
|
|
@@ -5997,7 +6032,7 @@ Next step: Run generate-app-icons with styleFolder='${styleFolder}' to create pl
|
|
|
5997
6032
|
};
|
|
5998
6033
|
}
|
|
5999
6034
|
try {
|
|
6000
|
-
|
|
6035
|
+
getGeminiClient();
|
|
6001
6036
|
} catch (error) {
|
|
6002
6037
|
return {
|
|
6003
6038
|
content: [
|
|
@@ -6014,7 +6049,8 @@ Next step: Run generate-app-icons with styleFolder='${styleFolder}' to create pl
|
|
|
6014
6049
|
baseIconPath,
|
|
6015
6050
|
outputPath,
|
|
6016
6051
|
stylePrompt,
|
|
6017
|
-
preserveShape
|
|
6052
|
+
preserveShape,
|
|
6053
|
+
imageModel
|
|
6018
6054
|
);
|
|
6019
6055
|
if (!stylizeResult.success) {
|
|
6020
6056
|
return {
|
|
@@ -6670,6 +6706,12 @@ function getToolZodSchema(name) {
|
|
|
6670
6706
|
}
|
|
6671
6707
|
|
|
6672
6708
|
// src/bin/mcp-server.ts
|
|
6709
|
+
console.log = function(...args) {
|
|
6710
|
+
console.error(...args);
|
|
6711
|
+
};
|
|
6712
|
+
console.info = function(...args) {
|
|
6713
|
+
console.error(...args);
|
|
6714
|
+
};
|
|
6673
6715
|
var server = new Server(
|
|
6674
6716
|
{
|
|
6675
6717
|
name: "pabal-resource-mcp",
|