hermoso 0.1.358 → 0.1.360
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 +21 -10
- package/package.json +1 -1
package/mcp/tools.mjs
CHANGED
|
@@ -17774,7 +17774,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
17774
17774
|
cameras: z.array(z.string()).optional().describe("host_episode: the rotation, ids frontal / three_quarter / close or framings in words; one entry = one fixed camera"),
|
|
17775
17775
|
thumbnail: z.boolean().optional().describe('host_episode: one thumbnail of the host (default true)'),
|
|
17776
17776
|
durationSeconds: z.number().optional().describe('target length 20-120s (default 60); drives the section count — ~10s of narration each, 3-8 sections'),
|
|
17777
|
-
frameDensity: z.enum(['standard', 'lean', 'minimal']).optional().describe("how many pictures per second of narration, and therefore what it costs. 'standard' (default) is a frame about every 1.5s — the density a stills film needs to read as a film rather than a slideshow; 'lean' is one about every 2.5s (the longest hold that still reads as a film, ~40% of the frames and ~40% of the cost); 'minimal' is
|
|
17777
|
+
frameDensity: z.enum(['standard', 'lean', 'minimal']).optional().describe("how many pictures per second of narration, and therefore what it costs. 'standard' (default) is a frame about every 1.5s — the density a stills film needs to read as a film rather than a slideshow; 'lean' is one about every 2.5s (the longest hold that still reads as a film, ~40% of the frames and ~40% of the cost); 'minimal' is one picture about every 3.5s, the cheapest and the longest any still is ever held, and it reads close to a slideshow. Only drop below the default if the user asked for something cheaper."),
|
|
17778
17778
|
aspectRatio: z.enum(['9:16', '16:9', '1:1', '4:5', '3:4']).optional().describe("'9:16' default"),
|
|
17779
17779
|
style: z.string().optional().describe("visual style: 'cinematic' (default, photoreal); styled shortcuts editorial_collage, flat_vector, stickman, whiteboard, ink_marker, silhouette, storybook, paper_diorama, isometric, claymation, pixel_art, watercolor, fluffy_toy, low_poly, stylized_3d, studio_3d (the Kids default), mannequin; or ANY look described in words ('80s anime cel animation'), locked across every frame. Ask rather than pick silently; a styled look costs more."),
|
|
17780
17780
|
channel: z.enum(['explainer', 'history', 'kids', 'fairytale']).optional().describe("the CHANNEL TYPE — it sets the pacing, the narration register and the default look, and is orthogonal to `style` (a named style always wins): explainer (casual second-person, fast cuts), history (witty chronological retelling / documentary), kids (fastest, question-first, warm teacher), fairytale (slow, atmospheric myth or folklore). Default 'explainer'."),
|
|
@@ -18564,19 +18564,27 @@ function memoryNoteVerdict(text) {
|
|
|
18564
18564
|
}));
|
|
18565
18565
|
server.registerTool('update_brand', {
|
|
18566
18566
|
title: 'Update brand fields',
|
|
18567
|
-
description: 'Patch SPECIFIC fields of the workspace brand profile (name, domain, sells, summary, category, audience, positioning, voice, style, goal) WITHOUT overwriting the rest — a read-modify-write on the saved brand. Use for “change our voice to playful”, “we sell to dentists now”. To onboard a brand from scratch, use draft_brand. Only pass the fields you’re changing.',
|
|
18567
|
+
description: 'Patch SPECIFIC fields of the workspace brand profile (name, domain, sells, summary, category, audience, positioning, voice, style, goal, and how the brand and product names are pronounced) WITHOUT overwriting the rest — a read-modify-write on the saved brand. Use for “change our voice to playful”, “we sell to dentists now”. To onboard a brand from scratch, use draft_brand. Only pass the fields you’re changing.',
|
|
18568
18568
|
inputSchema: {
|
|
18569
18569
|
name: z.string().optional(), domain: z.string().optional().describe('website domain'), sells: z.string().optional().describe('what the brand sells'),
|
|
18570
18570
|
summary: z.string().optional().describe('one-line description'), category: z.string().optional(), audience: z.string().optional(),
|
|
18571
18571
|
positioning: z.string().optional(), voice: z.string().optional().describe('brand voice/tone'), style: z.string().optional().describe('visual style — palette, typography, aesthetic'), goal: z.string().optional().describe('current marketing goal'),
|
|
18572
|
+
pronounce: z.string().optional().describe('how the brand NAME is said aloud, as a simple respelling with the stressed syllable in capitals (e.g. "KOH-dee-ak"). Videos use it as a delivery note beside the spoken line; set it when a render mispronounced the name.'),
|
|
18573
|
+
pronunciations: z.record(z.string()).optional().describe('how PRODUCT names are said aloud, e.g. {"Power Cakes": "POW-er cakes"}. Merged into the saved ones; an empty string removes one.'),
|
|
18572
18574
|
},
|
|
18573
18575
|
outputSchema: { ok: z.boolean().optional(), updated: z.array(z.string()).optional(), brand: z.any().optional() },
|
|
18574
18576
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
18575
18577
|
}, wrap(async (a) => {
|
|
18576
|
-
const allow = ['name', 'domain', 'sells', 'summary', 'category', 'audience', 'positioning', 'voice', 'style', 'goal'];
|
|
18578
|
+
const allow = ['name', 'domain', 'sells', 'summary', 'category', 'audience', 'positioning', 'voice', 'style', 'goal', 'pronounce'];
|
|
18577
18579
|
const patch = {}; for (const k of allow) if (a[k] != null && String(a[k]).trim()) patch[k] = String(a[k]).slice(0, 400);
|
|
18578
|
-
if (!Object.keys(patch).length) return { content: [{ type: 'text', text: 'Pass at least one brand field to change.' }], isError: true };
|
|
18579
18580
|
let brand = await readStore('heist.brand.v1'); if (!brand || typeof brand !== 'object' || Array.isArray(brand)) brand = {};
|
|
18581
|
+
// PRODUCT-NAME PRONUNCIATIONS merge into the saved map ('' removes one) — the user's correction of a derived hint.
|
|
18582
|
+
if (a.pronunciations && typeof a.pronunciations === 'object' && !Array.isArray(a.pronunciations)) {
|
|
18583
|
+
const cur = { ...((brand.pronunciations && typeof brand.pronunciations === 'object' && !Array.isArray(brand.pronunciations)) ? brand.pronunciations : {}) };
|
|
18584
|
+
for (const [k, v] of Object.entries(a.pronunciations).slice(0, 30)) { const n = String(k || '').trim().slice(0, 60); if (!n) continue; const say = String(v || '').trim().slice(0, 60); if (say) cur[n] = say; else delete cur[n]; }
|
|
18585
|
+
patch.pronunciations = cur;
|
|
18586
|
+
}
|
|
18587
|
+
if (!Object.keys(patch).length) return { content: [{ type: 'text', text: 'Pass at least one brand field to change.' }], isError: true };
|
|
18580
18588
|
const merged = { ...brand, ...patch };
|
|
18581
18589
|
await writeStore('heist.brand.v1', merged); // the store PUT preserves server-side brand enrichments (playbook/pronounce)
|
|
18582
18590
|
return ok(`Updated brand (${Object.keys(patch).join(', ')}).`, { ok: true, updated: Object.keys(patch), brand: merged });
|
|
@@ -20244,19 +20252,21 @@ function memoryNoteVerdict(text) {
|
|
|
20244
20252
|
// the engine; this wrapper only fills the brand's product photo. Held out of the default list on size (ON_DEMAND_TOOLS).
|
|
20245
20253
|
server.registerTool('recast_hook', {
|
|
20246
20254
|
title: 'Recast a viral hook with your subject',
|
|
20247
|
-
description: "RECAST A VIRAL HOOK
|
|
20255
|
+
description: "RECAST A VIRAL HOOK with the brand's own subject (the egg rolling off the table becomes your product; your host does the viral move), then join it into your clip. hook: a public post link (TikTok, Reel, Facebook, X, YouTube; one lookup) or a video file. Subject: creator (a saved creator's name or id), image (a product or a person; a real person's photo records their consent), or neither for the brand's first product photo. A DUO OR GROUP (2-4 people): subjects [{creator or image, position: 'left'|'right'|'center' or your own words, refs?, outfit?}], each bound to its place in the frame, keeping that performer's movement and timing on the original sound. LOCATION AND OUTFITS, same pass: location (a place photo or words) moves the scene; outfit (a photo or words) re-dresses the subject, subjects[].outfit each person; with no subject named nobody is swapped and only those change (useBrand:true also swaps in the brand product). Pictures are references, 4 in all (people or product, then location, then outfits); past 4 it is refused by name: put one in words. FULL LENGTH: past the editor's one pass (15 s with a face) the window is recast in passes, each continuing from the previous one's frame and crossfaded across the cut, under the original sound: one video up to 60 s, quoted as passes plus join; the reply names the moment to watch. mode 'auto' (default): 'swap' is a region edit that keeps camera, set, light, timing, on-screen text and sound; 'motion' makes your person perform the hook's moves (needs a person in the hook; not with location or outfit). swap: your own words for what changes. start/end pick the window (3 s minimum; default: a group or a location/outfit change takes the whole clip, one subject the first 10 s). PAID, QUOTED FIRST: without confirm it returns the plan and credits; call again with confirm:true once the user okays it. Then get_job until done, and post_edit with videoUrl = the result and ops [{op:'join', clips:[{url: your clip}], bridge:{kind:'impact'}}].",
|
|
20248
20256
|
inputSchema: {
|
|
20249
20257
|
hook: z.string().describe('the viral hook: a post link or a video URL'),
|
|
20250
20258
|
creator: z.string().optional().describe("a saved creator's name or id (list_creators)"),
|
|
20251
20259
|
image: z.string().optional().describe('a picture of the subject instead: a product or a person'),
|
|
20252
|
-
subjects: z.array(z.object({ creator: z.string().optional().describe("a saved creator's name or id"), image: z.string().optional().describe('or a picture of this person'), position: z.string().optional().describe("where they are in the hook: 'left', 'right', 'center', or your own words ('who raps first'); all or none"), refs: z.array(z.string()).optional().describe("up to 3 more views of the SAME person (a saved creator's pose plates ride automatically)"), name: z.string().optional() }).passthrough()).max(4).optional().describe('a duo or group recast, 2-4 people, instead of creator/image'),
|
|
20260
|
+
subjects: z.array(z.object({ creator: z.string().optional().describe("a saved creator's name or id"), image: z.string().optional().describe('or a picture of this person'), position: z.string().optional().describe("where they are in the hook: 'left', 'right', 'center', or your own words ('who raps first'); all or none"), refs: z.array(z.string()).optional().describe("up to 3 more views of the SAME person (a saved creator's pose plates ride automatically)"), name: z.string().optional(), outfit: z.string().optional().describe('what this person wears instead: an outfit photo URL or words; alone (no creator/image) it keeps this person and changes only the clothes') }).passthrough()).max(4).optional().describe('a duo or group recast, 2-4 people, instead of creator/image'),
|
|
20261
|
+
location: z.string().optional().describe('move the scene: a photo URL of the place, or words ("a sunny beach at golden hour")'),
|
|
20262
|
+
outfit: z.string().optional().describe('what the subject (or, with no subject, the main person in the hook) wears instead: an outfit photo URL or words'),
|
|
20253
20263
|
swap: z.string().optional().describe('what changes, in your own words'),
|
|
20254
20264
|
mode: z.enum(['auto', 'swap', 'motion']).optional(),
|
|
20255
20265
|
start: z.number().optional(), end: z.number().optional(),
|
|
20256
20266
|
prompt: z.string().optional().describe('extra direction for the render'),
|
|
20257
20267
|
model: z.string().optional().describe('swap: a clip editor id (hermoso_capabilities); omit for the default'),
|
|
20258
20268
|
tier: z.enum(['pro', 'standard']).optional().describe('motion: the motion-transfer tier (default pro)'),
|
|
20259
|
-
useBrand: z.boolean().optional().describe('false = no brand product photo'),
|
|
20269
|
+
useBrand: z.boolean().optional().describe('false = no brand product photo; true with location/outfit and no subject = also swap in the brand product'),
|
|
20260
20270
|
confirm: z.boolean().optional().describe('true = render it (after the user okayed the quote)'),
|
|
20261
20271
|
},
|
|
20262
20272
|
outputSchema: { plan: z.any().optional(), job: z.any().optional(), credits: z.number().optional(), queued: z.boolean().optional() },
|
|
@@ -20264,12 +20274,13 @@ function memoryNoteVerdict(text) {
|
|
|
20264
20274
|
}, wrap(async (a) => {
|
|
20265
20275
|
let b = {};
|
|
20266
20276
|
const subs = Array.isArray(a.subjects) ? a.subjects.filter(Boolean) : [];
|
|
20267
|
-
|
|
20277
|
+
const sceneOnly = !a.creator && !a.image && !subs.some((x) => x && (x.creator || x.image)) && (a.location || a.outfit || subs.some((x) => x && x.outfit)); // a location / outfit change with nobody swapped
|
|
20278
|
+
if (a.useBrand !== false && !a.creator && !a.image && !subs.length && (!sceneOnly || a.useBrand === true)) { try { b = (await readStore('heist.brand.v1')) || {}; } catch { b = {}; } }
|
|
20268
20279
|
// a person's photo as the subject is recorded as implied likeness consent SERVER-side, only when the free face check finds a face there (a product photo is not a likeness)
|
|
20269
|
-
const body = { hook: a.hook, ...(a.creator ? { creator: a.creator } : {}), ...(a.image ? { image: a.image } : {}), ...(subs.length ? { subjects: subs } : {}), ...(a.swap ? { swap: a.swap } : {}), ...(a.mode && a.mode !== 'auto' ? { mode: a.mode } : {}), ...(a.start != null ? { start: a.start } : {}), ...(a.end != null ? { end: a.end } : {}), ...(a.prompt ? { prompt: a.prompt } : {}), ...(a.model ? { model: a.model } : {}), ...(a.tier ? { tier: a.tier } : {}), ...(b && Array.isArray(b.productImages) && b.productImages.length ? { brand: { name: b.name || '', product: b.sells || b.name || '', productImages: b.productImages.slice(0, 1) } } : {}), ...(a.confirm === true ? { confirm: true } : {}) };
|
|
20280
|
+
const body = { hook: a.hook, ...(a.creator ? { creator: a.creator } : {}), ...(a.image ? { image: a.image } : {}), ...(subs.length ? { subjects: subs } : {}), ...(a.location ? { location: a.location } : {}), ...(a.outfit ? { outfit: a.outfit } : {}), ...(sceneOnly && a.useBrand === true ? { useBrand: true } : {}), ...(a.swap ? { swap: a.swap } : {}), ...(a.mode && a.mode !== 'auto' ? { mode: a.mode } : {}), ...(a.start != null ? { start: a.start } : {}), ...(a.end != null ? { end: a.end } : {}), ...(a.prompt ? { prompt: a.prompt } : {}), ...(a.model ? { model: a.model } : {}), ...(a.tier ? { tier: a.tier } : {}), ...(b && Array.isArray(b.productImages) && b.productImages.length ? { brand: { name: b.name || '', product: b.sells || b.name || '', productImages: b.productImages.slice(0, 1) } } : {}), ...(a.confirm === true ? { confirm: true } : {}) };
|
|
20270
20281
|
const r = await apiPost('/api/hooks/recast', body);
|
|
20271
20282
|
const p = r?.data || r;
|
|
20272
|
-
const head = `${p.mode === 'motion' ? 'Motion transfer' : 'Swap'} on ${p.engine}: ${p.hook?.window?.seconds}s of the hook (${p.hook?.window?.start}-${p.hook?.window?.end}s of ${p.hook?.durationSeconds}s). ${Array.isArray(p.subjects) ? `Subjects: ${p.subjects.map((x) => `${x.element} ${x.position}${x.name ? ` "${x.name}"` : ''} (${x.kind}${x.refs ? `, ${x.refs} more view${x.refs === 1 ? '' : 's'}` : ''})`).join('; ')}` : `Subject: ${p.subject?.kind}${p.subject?.name ? ` "${p.subject.name}"` : ''}`}. Face in the hook: ${p.hook?.face}; in the subject: ${p.subject?.face}. Why: ${p.why}.${Array.isArray(p.passes) ? `\nPasses: ${p.passes.map((w) => `${w.start}-${w.end}s${w.credits != null ? ` (${w.credits} credits)` : ''}`).join(', ')}; join${p.join?.credits != null ? ` (${p.join.credits} credits)` : ''}: ${p.join?.how || 'a hard cut'}.` : ''}${p.windowNote ? `\n${p.windowNote}` : ''}${p.instruction ? `\nInstruction: ${p.instruction}` : ''}${p.consent ? `\n${p.consent}` : ''}`;
|
|
20283
|
+
const head = `${p.mode === 'motion' ? 'Motion transfer' : 'Swap'} on ${p.engine}: ${p.hook?.window?.seconds}s of the hook (${p.hook?.window?.start}-${p.hook?.window?.end}s of ${p.hook?.durationSeconds}s). ${Array.isArray(p.subjects) ? `Subjects: ${p.subjects.map((x) => `${x.element} ${x.position}${x.name ? ` "${x.name}"` : ''} (${x.kind}${x.refs ? `, ${x.refs} more view${x.refs === 1 ? '' : 's'}` : ''})`).join('; ')}` : (p.subject?.kind === 'kept' ? 'Nobody is swapped' : `Subject: ${p.subject?.kind}${p.subject?.name ? ` "${p.subject.name}"` : ''}`)}${p.location ? `; location: ${p.location.kind === 'photo' ? p.location.image : p.location.text}` : ''}${p.outfit ? `; outfit: ${p.outfit.kind === 'photo' ? p.outfit.image : p.outfit.text}` : ''}${Array.isArray(p.outfits) ? `; outfits: ${p.outfits.map((o) => `${o.who}: ${o.kind === 'photo' ? o.image : o.text}`).join('; ')}` : ''}. Face in the hook: ${p.hook?.face}; in the subject: ${p.subject?.face}. Why: ${p.why}.${Array.isArray(p.passes) ? `\nPasses: ${p.passes.map((w) => `${w.start}-${w.end}s${w.credits != null ? ` (${w.credits} credits)` : ''}`).join(', ')}; join${p.join?.credits != null ? ` (${p.join.credits} credits)` : ''}: ${p.join?.how || 'a hard cut'}.` : ''}${p.windowNote ? `\n${p.windowNote}` : ''}${p.instruction ? `\nInstruction: ${p.instruction}` : ''}${p.consent ? `\n${p.consent}` : ''}`;
|
|
20273
20284
|
if (!p.queued) return { content: [{ type: 'text', text: `QUOTE (nothing rendered, nothing charged): about ${p.credits} credits.\n${head}\nTell the user the price; after they okay it, call recast_hook again with the same inputs and confirm:true.` }], structuredContent: { plan: p, credits: p.credits, queued: false } };
|
|
20274
20285
|
return { content: [{ type: 'text', text: `Recast queued as job ${p.job?.id} (about ${p.credits} credits).\n${head}\nCall get_job until it reports done; it has no file until then. ${p.next || ''}` }], structuredContent: { plan: p, job: p.job, credits: p.credits, queued: true } };
|
|
20275
20286
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.360",
|
|
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",
|