hermoso 0.1.428 → 0.1.429
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 +14 -9
- package/package.json +1 -1
package/mcp/tools.mjs
CHANGED
|
@@ -18404,10 +18404,15 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
18404
18404
|
// image may be an https URL (an upload_file URL included), a Library id from list_library (L0, L1… newest first), or a
|
|
18405
18405
|
// local path on the CLI (sent as bytes, stored once by the server). An id or path that resolves to nothing is refused
|
|
18406
18406
|
// by name, free, before anything is planned or rendered.
|
|
18407
|
+
// HOW IT LOOKS (2026-10-19): a screen sits in a rounded card over the footage, which keeps playing around it (look
|
|
18408
|
+
// 'card', the default; 'full' covers the frame, only when the user asked); a photo of a person, product or place (kind
|
|
18409
|
+
// 'photo') is never laid over the film. Never in the first second or the last 1.5 s; 2 s each at most (lib/insert-windows.mjs).
|
|
18407
18410
|
const INSERTS_Z = z.array(z.object({
|
|
18408
18411
|
image: z.string().describe('an https or upload_file URL, a Library id (L0, L1…) or a local path on the CLI'),
|
|
18409
|
-
start: z.number().describe('second it appears'),
|
|
18410
|
-
end: z.number().describe('second it ends (
|
|
18412
|
+
start: z.number().describe('second it appears (never before 1 s: the film opens on moving footage)'),
|
|
18413
|
+
end: z.number().describe('second it ends: at most 2 s after start (2.5 s full-screen), never in the last 1.5 s of the film'),
|
|
18414
|
+
kind: z.enum(['screen', 'photo']).optional().describe("screen (default): an app, a page, a dashboard, a document, a review, a chart. photo: a person, a product or a place, which is NOT laid over the video (a product photo goes in as the brand's product photo, a person to star as a saved creator)"),
|
|
18415
|
+
look: z.enum(['card', 'full']).optional().describe("card (default): a rounded card over the upper middle of the footage, which keeps playing. full: covers the whole frame, ONLY when the user asked for full-screen"),
|
|
18411
18416
|
})).max(8);
|
|
18412
18417
|
const insertsResolved = async (list) => {
|
|
18413
18418
|
if (!Array.isArray(list)) return undefined;
|
|
@@ -18424,11 +18429,11 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
18424
18429
|
image = String(a.url);
|
|
18425
18430
|
} else image = await toRef(raw);
|
|
18426
18431
|
if (!image) throw Object.assign(new Error(`The picture "${raw.slice(0, 80)}" could not be read: pass an https URL, an upload_file URL or a Library id (L0…). Nothing was planned, rendered or charged.`), { status: 400, _userInput: true });
|
|
18427
|
-
out.push({ image: /^\/generated\//.test(image) ? abs(image) : image, start: +x.start, end: +x.end });
|
|
18432
|
+
out.push({ image: /^\/generated\//.test(image) ? abs(image) : image, start: +x.start, end: +x.end, ...(x.kind === 'photo' ? { kind: 'photo' } : {}), ...(x.look === 'full' ? { look: 'full' } : {}) });
|
|
18428
18433
|
}
|
|
18429
18434
|
return out;
|
|
18430
18435
|
};
|
|
18431
|
-
const insertsLine = (ins, verb = 'are laid') => (Array.isArray(ins) && ins.length ? `\nCut-ins: ${ins.length === 1 ? 'the user\'s picture' : `${ins.length} of the user's pictures`} ${ins.length === 1 ? verb.replace(/^are/, 'is') : verb}
|
|
18436
|
+
const insertsLine = (ins, verb = 'are laid') => (Array.isArray(ins) && ins.length ? `\nCut-ins: ${ins.length === 1 ? 'the user\'s picture' : `${ins.length} of the user's pictures`} ${ins.length === 1 ? verb.replace(/^are/, 'is') : verb} at ${ins.map((x) => `${x.start}–${x.end}s${x.look === 'full' ? ' (full-screen)' : ''}`).join(', ')}${ins.some((x) => x.look !== 'full') ? ', as a card over the footage, which keeps playing' : ''}.` : '');
|
|
18432
18437
|
server.registerTool('plan_ad', {
|
|
18433
18438
|
title: 'Plan an ad concept',
|
|
18434
18439
|
description: 'Creative director: turn a brand + product/brief into a finished ad CONCEPT — copy variants (headline/primary/cta) plus an image_concept.prompt OR a video_storyboard, with the resolved recipe + the model ids to render with. Renders nothing; chain its output into generate_image / generate_video. THE USER’S EXPLICIT LENGTH IS SOVEREIGN: when they name a duration ("a 30 second ad", "make it 45s"), pass it as durationSeconds — the board is then AUTHORED to that length (its scenes sum to it) and render_ad renders it as one clip or stitched acts accordingly. Leaving it out lets the planner pick its own default, which is how an explicit ask silently becomes a 15s spot. The user’s OWN screenshots or pictures cut into the video full-screen go in `inserts`: the board keeps those seconds free of any screen, and render_ad lays the real pictures in. Spends credits.',
|
|
@@ -18444,12 +18449,12 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
18444
18449
|
language: z.string().optional().describe('output language for the ad copy (e.g. Spanish) — default English'),
|
|
18445
18450
|
talent: z.enum(['auto', 'creator', 'product_only']).optional().describe("who is on camera; omit to follow the format. Say 'someone new' in product to skip reusing a saved creator."),
|
|
18446
18451
|
draft: z.object({ model: z.string(), durationSeconds: z.number() }).optional().describe("ONLY after a video refusal that offered a light draft: the {model, durationSeconds} it named. The plan is then authored to that length and priced on that model. Never invent one — a video the account cannot cover is refused BEFORE planning with the three options (image / add credits / this draft when one fits), and the user chooses."),
|
|
18447
|
-
inserts: INSERTS_Z.optional().describe("VIDEO ONLY: the user's own
|
|
18452
|
+
inserts: INSERTS_Z.optional().describe("VIDEO ONLY: the user's own SCREENS (an app, a page, a dashboard, a document, a review, a chart) shown at these seconds as a rounded card over the upper middle of the footage, which keeps playing around it (look 'full' covers the frame, only when the user asked), the voice running underneath. Fewer and shorter: about one per 4 s of film, 1.2-2 s each, on the line that talks about it; never in the first second or the last 1.5 s. The plan frames those shots for the card, keeps them free of screens, and returns the pictures as creative.inserts for render_ad to lay in (about 2 credits). A photo of a person, product or place (kind 'photo') is not laid in: pass a product photo as the brand's product photo and a person as a saved creator. One that does not fit is left out and the reply says so."),
|
|
18448
18453
|
},
|
|
18449
18454
|
outputSchema: {
|
|
18450
18455
|
format: z.string().optional().describe("the resolved creative format — 'image' or 'video'"),
|
|
18451
|
-
inserts: z.array(z.any()).optional().describe("the user's pictures render_ad will lay in
|
|
18452
|
-
inserts_note: z.string().optional().describe('set when a picture was left out
|
|
18456
|
+
inserts: z.array(z.any()).optional().describe("the user's pictures render_ad will lay in: [{image, start, end, look}] (look card = over the playing footage, full = full-screen), fitted to the planned length"),
|
|
18457
|
+
inserts_note: z.string().optional().describe('set when a picture was left out (the planned length has no room for it, or it was a photo, which is never laid over the video)'),
|
|
18453
18458
|
concept: z.string().optional().describe('the one-line creative concept'),
|
|
18454
18459
|
format_note: z.string().optional().describe("the plan's own read-back when it chose an IMAGE on an open format because the balance could not cover the default video — states the video's real credit price"),
|
|
18455
18460
|
budget_pick: z.any().optional().describe('{format, reason, videoCredits, balance, seconds} when format_note is set'),
|
|
@@ -18868,7 +18873,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
18868
18873
|
ttsVoice: z.string().optional().describe('voiceover voice name (e.g. Rachel / George) when the plan voices over'),
|
|
18869
18874
|
dryRun: z.boolean().optional().describe('return the routing decision (single pass vs stitched acts, resolved model + act lengths) and the estimated credits the real render reserves, WITHOUT submitting a render — free, nothing charged'),
|
|
18870
18875
|
allowGenericProduct: z.boolean().optional().describe('proceed even though this brand has NO product photo on file and the ad features a product — the packaging will be INVENTED. Only pass true after telling the user that and hearing they are fine with a generic stand-in'),
|
|
18871
|
-
inserts: INSERTS_Z.optional().describe("the user's own
|
|
18876
|
+
inserts: INSERTS_Z.optional().describe("the user's own screens laid over the finished video at these seconds, as a card over the playing footage by default (look 'full' covers the frame, only when asked), sound running underneath (about 2 credits). The film always opens and ends on moving footage. Omit to use plan_ad's creative.inserts; [] renders without them. plan_ad with inserts writes the board around them."),
|
|
18872
18877
|
},
|
|
18873
18878
|
outputSchema: {
|
|
18874
18879
|
...JOB_OUT,
|
|
@@ -19565,7 +19570,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
19565
19570
|
}
|
|
19566
19571
|
const seamLine = seamsText(res); // a timeline / join's measured seams + intro budget ('' for any other video)
|
|
19567
19572
|
// a render that laid the user's pictures in (render_ad inserts): say they are in it, or that they could not be
|
|
19568
|
-
const insLine = res?.inserts?.laid ? `\nCut-ins: ${res.inserts.laid === 1 ? 'the user\'s picture is' : `${res.inserts.laid} of the user's pictures are`} laid in
|
|
19573
|
+
const insLine = res?.inserts?.laid ? `\nCut-ins: ${res.inserts.laid === 1 ? 'the user\'s picture is' : `${res.inserts.laid} of the user's pictures are`} laid in at ${(res.inserts.windows || []).map((x) => `${x.start}–${x.end}s${x.look === 'full' ? ' (full-screen)' : ''}`).join(', ')}.` : (res?.inserts?.failed && res.note ? `\nTELL THE USER: ${res.note}` : '');
|
|
19569
19574
|
if (j.status === 'done' && res?.video) return okVideo(wireText + seamLine + insLine, { ...j, url }); // resumed video → same inline poster as a direct return
|
|
19570
19575
|
if (j.status === 'done' && res?.image) { const img = await imageBlock(url); return { content: [{ type: 'text', text: wireText }, ...(img ? [img] : [])], structuredContent: { ...j, url } }; }
|
|
19571
19576
|
return ok(wireText, { ...j, url });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.429",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "AI marketing agent and AI ad generator, run from your own AI agent. 922 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, ChatGPT Ads and AppLovin Ads, plus product feeds in Google Merchant Center. PUBLISHING AND SCHEDULING: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky, Telegram and Google Business Profile. INSTAGRAM DM AUTOMATIONS: a keyword comment, a DM, a story reply or an ig.me link gets an automatic private reply from the brand. 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",
|