hermoso 0.1.335 → 0.1.336
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/mcp/tools.mjs +20 -15
- package/package.json +1 -1
package/mcp/tools.mjs
CHANGED
|
@@ -17156,75 +17156,80 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
17156
17156
|
// balance cannot cover it.
|
|
17157
17157
|
const staticItemsText = (d, label) => {
|
|
17158
17158
|
const items = Array.isArray(d.items) ? d.items : [];
|
|
17159
|
-
const rows = items.map((x, i) => `${i + 1}. ${x[label] || ''}${x.image ? ` — ${abs(x.image)}` : ` — not delivered: ${x.error || 'failed'}`}${x.textCheck && x.textCheck.ok === false ? `\n ⚠ ${x.textCheck.note}` : ''}`);
|
|
17159
|
+
const rows = items.map((x, i) => `${i + 1}. ${x[label] || ''}${x.image ? ` — ${abs(x.image)}` : ` — not delivered: ${x.error || 'failed'}`}${x.textCheck && x.textCheck.ok === false ? `\n ⚠ ${x.textCheck.note}` : ''}${x.labelPass && x.labelPass.note ? `\n ${x.labelPass.note}` : ''}`);
|
|
17160
17160
|
return `${rows.join('\n')}\n${d.note || ''}`;
|
|
17161
17161
|
};
|
|
17162
17162
|
const staticImages = async (d) => (await Promise.all((Array.isArray(d.items) ? d.items : []).filter(x => x.image).slice(0, 4).map(x => imageBlock(abs(x.image))))).filter(Boolean);
|
|
17163
17163
|
const staticItemsOut = z.array(z.any()).optional().describe('one entry per output: the image URL (or an error), what changed, and a textCheck flag when the rendered text may not match');
|
|
17164
17164
|
server.registerTool('edit_image', {
|
|
17165
17165
|
title: 'Edit an image',
|
|
17166
|
-
description: "EDIT an existing image in place with a plain-language instruction and keep everything else: 'make the headline bigger', 'add our logo bottom right', 'swap the background for a kitchen', 'remove the person on the left', 'erase all the text'. Pass `image` (URL, Library item, upload_file URL or local path) and `instruction`. The same edit the web Studio's Edit runs: composition, aspect ratio, people and every untouched line of text stay as they are; the saved brand's real name and website are pinned so an added line never invents one, and the brand's real logo is attached when the instruction asks for the logo. Set removal:true when the edit STRIPS text, branding or an object, so nothing branded is put back. For a precise region, pass `mask` (see generate_image). One image edit's credits; returns the new image URL. For a new image from a prompt use generate_image; to rebuild a competitor's ad for your brand use clone_static.",
|
|
17166
|
+
description: "EDIT an existing image in place with a plain-language instruction and keep everything else: 'make the headline bigger', 'add our logo bottom right', 'swap the background for a kitchen', 'remove the person on the left', 'erase all the text'. Pass `image` (URL, Library item, upload_file URL or local path) and `instruction`. The same edit the web Studio's Edit runs: composition, aspect ratio, people and every untouched line of text stay as they are; the saved brand's real name and website are pinned so an added line never invents one, and the brand's real logo is attached when the instruction asks for the logo. Set removal:true when the edit STRIPS text, branding or an object, so nothing branded is put back. When the saved brand has a product photo and the ad shows that product, the photo rides with the edit and the product's label is read and checked against it: re-printed from the photo only when it came out wrong (the check alone is charged when it is right; one small extra charge finds the product first). fixLabel:false turns that off. For a precise region, pass `mask` (see generate_image). One image edit's credits; returns the new image URL. For a new image from a prompt use generate_image; to rebuild a competitor's ad for your brand use clone_static.",
|
|
17167
17167
|
inputSchema: {
|
|
17168
17168
|
image: z.string().describe('the image to edit: URL, Library item URL, upload_file URL or local path'),
|
|
17169
17169
|
instruction: z.string().describe('the change to make, in plain words (pass the user’s own words for a removal or plain photo edit)'),
|
|
17170
17170
|
removal: z.boolean().optional().describe('true when the edit REMOVES text, branding, a logo, a watermark, a person or an object, so the brand name and logo are not re-added'),
|
|
17171
17171
|
mask: z.string().optional().describe('optional mask image (URL or local path) marking the region to change: transparent = change, or white = change on an opaque mask'),
|
|
17172
17172
|
dryRun: z.boolean().optional().describe('true = return the exact credits this edit reserves and render nothing'),
|
|
17173
|
+
fixLabel: z.boolean().optional().describe('false = leave the product label exactly as rendered: no product lookup, no label check, no re-print (default on when the ad shows the brand\u2019s saved product)'),
|
|
17173
17174
|
},
|
|
17174
17175
|
outputSchema: {
|
|
17175
17176
|
image: z.string().optional().describe('the served absolute URL of the edited image'),
|
|
17176
17177
|
model: z.string().optional().describe('the model label that rendered it'),
|
|
17177
17178
|
},
|
|
17178
17179
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
17179
|
-
}, wrap(async ({ image, instruction, removal, mask, dryRun }) => {
|
|
17180
|
+
}, wrap(async ({ image, instruction, removal, mask, dryRun, fixLabel }) => {
|
|
17180
17181
|
const src = await toRef(image);
|
|
17181
17182
|
const maskRef = mask ? await toRef(mask) : undefined;
|
|
17182
|
-
|
|
17183
|
-
const d = await apiPost('/api/static/edit', { image: src, instruction, ...(removal === true ? { removal: true } : {}), ...(maskRef ? { mask: maskRef } : {}) });
|
|
17183
|
+
const fl = fixLabel === false ? { fixLabel: false } : {};
|
|
17184
|
+
if (dryRun) { const d = await apiPost('/api/static/edit', { image: src, instruction, ...(removal === true ? { removal: true } : {}), ...(maskRef ? { mask: maskRef } : {}), ...fl, dryRun: true }); return ok(quoteText(d && d.quote, 'This image edit (a change to the existing image, not a new render)'), {}); }
|
|
17185
|
+
const d = await apiPost('/api/static/edit', { image: src, instruction, ...(removal === true ? { removal: true } : {}), ...(maskRef ? { mask: maskRef } : {}), ...fl });
|
|
17184
17186
|
const img = await imageBlock(abs(d.image));
|
|
17185
|
-
return { content: [{ type: 'text', text: `Edited image: ${abs(d.image)}${d.model ? ` (${d.model})` : ''}` }, ...(img ? [img] : [])], structuredContent: { ...d, image: abs(d.image) } };
|
|
17187
|
+
return { content: [{ type: 'text', text: `Edited image: ${abs(d.image)}${d.model ? ` (${d.model})` : ''}${d.productNote ? `\n${d.productNote}` : ''}` }, ...(img ? [img] : [])], structuredContent: { ...d, image: abs(d.image) } };
|
|
17186
17188
|
}));
|
|
17187
17189
|
server.registerTool('headline_variants', {
|
|
17188
17190
|
title: 'Headline variants of a static ad',
|
|
17189
|
-
description: "Turn ONE finished static ad into several copies that differ ONLY in the headline, for an A/B test: same picture, product, layout, colours and every other line. Pass `image`, and either `headlines` (your own, up to 10) or `count` (default 5, max 10) to have distinct angles written for you in the saved brand's voice (never inventing numbers, prices, ratings or claims the ad or brand does not state); `brief` steers what to test. The ad's text is read first (3 credits), then one image edit per headline; each output is proofread and flagged (textCheck) if the rendered words do not match, never silently re-rendered. The whole batch is priced before anything runs. Returns each headline, its angle and its image URL.",
|
|
17191
|
+
description: "Turn ONE finished static ad into several copies that differ ONLY in the headline, for an A/B test: same picture, product, layout, colours and every other line. Pass `image`, and either `headlines` (your own, up to 10) or `count` (default 5, max 10) to have distinct angles written for you in the saved brand's voice (never inventing numbers, prices, ratings or claims the ad or brand does not state); `brief` steers what to test. The ad's text is read first (3 credits), then one image edit per headline; each output is proofread and flagged (textCheck) if the rendered words do not match, never silently re-rendered. When the saved brand has a product photo and the ad shows that product, the photo rides with the edit and the product's label is read and checked against it: re-printed from the photo only when it came out wrong (the check alone is charged when it is right; one small extra charge finds the product first). fixLabel:false turns that off. The whole batch is priced before anything runs. Returns each headline, its angle and its image URL.",
|
|
17190
17192
|
inputSchema: {
|
|
17191
17193
|
image: z.string().describe('the finished static ad: URL, Library item URL, upload_file URL or local path'),
|
|
17192
17194
|
headlines: z.array(z.string()).optional().describe('your own headlines to test (up to 10); omit to have them written'),
|
|
17193
17195
|
count: z.number().int().min(1).max(10).optional().describe('how many headlines to write when `headlines` is omitted (default 5)'),
|
|
17194
17196
|
brief: z.string().optional().describe('what to test, e.g. "price-led vs outcome-led" or "speak to busy parents"'),
|
|
17197
|
+
fixLabel: z.boolean().optional().describe('false = leave the product label exactly as rendered: no product lookup, no label check, no re-print (default on when the ad shows the brand\u2019s saved product)'),
|
|
17195
17198
|
},
|
|
17196
17199
|
outputSchema: { items: staticItemsOut, note: z.string().optional(), original: z.string().optional().describe('the headline read off the source ad') },
|
|
17197
17200
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
17198
|
-
}, wrap(async ({ image, headlines, count, brief }) => {
|
|
17199
|
-
const d = await apiPost('/api/static/headlines', { image: await toRef(image), ...(headlines?.length ? { headlines } : {}), ...(count ? { count } : {}), ...(brief ? { brief } : {}) });
|
|
17201
|
+
}, wrap(async ({ image, headlines, count, brief, fixLabel }) => {
|
|
17202
|
+
const d = await apiPost('/api/static/headlines', { image: await toRef(image), ...(headlines?.length ? { headlines } : {}), ...(count ? { count } : {}), ...(brief ? { brief } : {}), ...(fixLabel === false ? { fixLabel: false } : {}) });
|
|
17200
17203
|
const imgs = await staticImages(d);
|
|
17201
17204
|
return { content: [{ type: 'text', text: `Headline variants (original: “${d.original || ''}”):\n${staticItemsText(d, 'headline')}` }, ...imgs], structuredContent: { ...d, items: (d.items || []).map(x => ({ ...x, image: x.image ? abs(x.image) : null })) } };
|
|
17202
17205
|
}));
|
|
17203
17206
|
server.registerTool('resize_ad', {
|
|
17204
17207
|
title: 'Resize a static ad for other placements',
|
|
17205
|
-
description: "Re-lay out ONE finished static ad for other placements: the same ad, product, copy (word for word), logo and style, recomposed natively for each canvas rather than cropped. Pass `image` and optionally `aspectRatios` from 1:1, 4:5, 9:16, 16:9, 3:4, 4:3 (default 1:1, 4:5 and 9:16; the ad's own ratio is skipped). Reads the ad's text first (3 credits) so every line survives, then one image edit per canvas. Priced before it runs. For VIDEO use reframe_video.",
|
|
17208
|
+
description: "Re-lay out ONE finished static ad for other placements: the same ad, product, copy (word for word), logo and style, recomposed natively for each canvas rather than cropped. Pass `image` and optionally `aspectRatios` from 1:1, 4:5, 9:16, 16:9, 3:4, 4:3 (default 1:1, 4:5 and 9:16; the ad's own ratio is skipped). Reads the ad's text first (3 credits) so every line survives, then one image edit per canvas. When the saved brand has a product photo and the ad shows that product, the photo rides with the edit and the product's label is read and checked against it: re-printed from the photo only when it came out wrong (the check alone is charged when it is right; one small extra charge finds the product first). fixLabel:false turns that off. Priced before it runs. For VIDEO use reframe_video.",
|
|
17206
17209
|
inputSchema: {
|
|
17207
17210
|
image: z.string().describe('the finished static ad: URL, Library item URL, upload_file URL or local path'),
|
|
17208
17211
|
aspectRatios: z.array(z.enum(['1:1', '4:5', '9:16', '16:9', '3:4', '4:3'])).optional().describe('target canvases (default 1:1, 4:5, 9:16)'),
|
|
17212
|
+
fixLabel: z.boolean().optional().describe('false = leave the product label exactly as rendered: no product lookup, no label check, no re-print (default on when the ad shows the brand\u2019s saved product)'),
|
|
17209
17213
|
},
|
|
17210
17214
|
outputSchema: { items: staticItemsOut, note: z.string().optional(), sourceAspectRatio: z.string().optional() },
|
|
17211
17215
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
17212
|
-
}, wrap(async ({ image, aspectRatios }) => {
|
|
17213
|
-
const d = await apiPost('/api/static/resize', { image: await toRef(image), ...(aspectRatios?.length ? { aspectRatios } : {}) });
|
|
17216
|
+
}, wrap(async ({ image, aspectRatios, fixLabel }) => {
|
|
17217
|
+
const d = await apiPost('/api/static/resize', { image: await toRef(image), ...(aspectRatios?.length ? { aspectRatios } : {}), ...(fixLabel === false ? { fixLabel: false } : {}) });
|
|
17214
17218
|
const imgs = await staticImages(d);
|
|
17215
17219
|
return { content: [{ type: 'text', text: `Resized (source ${d.sourceAspectRatio || '?'}):\n${staticItemsText(d, 'aspectRatio')}` }, ...imgs], structuredContent: { ...d, items: (d.items || []).map(x => ({ ...x, image: x.image ? abs(x.image) : null })) } };
|
|
17216
17220
|
}));
|
|
17217
17221
|
server.registerTool('localize_ad', {
|
|
17218
17222
|
title: 'Localize a static ad into other languages',
|
|
17219
|
-
description: "Translate the on-image text of ONE finished static ad into other languages and keep everything else: same picture, layout, typeface, colours, logo and product. Pass `image` and `languages` (up to 5, e.g. [\"Spanish\", \"German\", \"French (Canada)\"]). The ad's text is read (3 credits), translated the way a native copywriter in each market would write it (brand and product names, URLs and prices kept as written), then one image edit per language; each output is proofread and flagged (textCheck) if the words do not match, never silently re-rendered. Priced before it runs. For a VIDEO use dub_video.",
|
|
17223
|
+
description: "Translate the on-image text of ONE finished static ad into other languages and keep everything else: same picture, layout, typeface, colours, logo and product. Pass `image` and `languages` (up to 5, e.g. [\"Spanish\", \"German\", \"French (Canada)\"]). The ad's text is read (3 credits), translated the way a native copywriter in each market would write it (brand and product names, URLs and prices kept as written), then one image edit per language; each output is proofread and flagged (textCheck) if the words do not match, never silently re-rendered. When the saved brand has a product photo and the ad shows that product, the photo rides with the edit and the product's label is read and checked against it: re-printed from the photo only when it came out wrong (the check alone is charged when it is right; one small extra charge finds the product first). fixLabel:false turns that off. Priced before it runs. For a VIDEO use dub_video.",
|
|
17220
17224
|
inputSchema: {
|
|
17221
17225
|
image: z.string().describe('the finished static ad: URL, Library item URL, upload_file URL or local path'),
|
|
17222
17226
|
languages: z.array(z.string()).min(1).max(5).describe('target languages, by name'),
|
|
17227
|
+
fixLabel: z.boolean().optional().describe('false = leave the product label exactly as rendered: no product lookup, no label check, no re-print (default on when the ad shows the brand\u2019s saved product)'),
|
|
17223
17228
|
},
|
|
17224
17229
|
outputSchema: { items: staticItemsOut, note: z.string().optional(), source: z.array(z.string()).optional().describe('the lines read off the source ad') },
|
|
17225
17230
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
17226
|
-
}, wrap(async ({ image, languages }) => {
|
|
17227
|
-
const d = await apiPost('/api/static/localize', { image: await toRef(image), languages });
|
|
17231
|
+
}, wrap(async ({ image, languages, fixLabel }) => {
|
|
17232
|
+
const d = await apiPost('/api/static/localize', { image: await toRef(image), languages, ...(fixLabel === false ? { fixLabel: false } : {}) });
|
|
17228
17233
|
const imgs = await staticImages(d);
|
|
17229
17234
|
return { content: [{ type: 'text', text: `Localized:\n${staticItemsText(d, 'language')}` }, ...imgs], structuredContent: { ...d, items: (d.items || []).map(x => ({ ...x, image: x.image ? abs(x.image) : null })) } };
|
|
17230
17235
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.336",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "Marketing on autopilot, run from your own AI agent. 864 tools. Publishing, scheduling, ad campaign management, comments, DMs and analytics cost no credits on every plan; credits are only for generating creative and for Ad Spy research. AD PLATFORMS: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads, Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads and ChatGPT Ads, plus product feeds in Google Merchant Center. PUBLISHING AND SCHEDULING: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky and Telegram. AD RESEARCH: the Meta, Google and LinkedIn ad libraries plus organic TikTok, Instagram, YouTube, Threads and Reddit. ANALYTICS: Google Analytics 4, Google Search Console and every connected platform's own post and campaign insights. Also brand onboarding, 50+ image and video generation models, ad scoring, competitor teardowns, Google Drive and OneDrive, a CLI and installable Claude skills.",
|
|
6
6
|
"type": "module",
|