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.
Files changed (2) hide show
  1. package/mcp/tools.mjs +21 -10
  2. 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 ONE picture per narration section, which is cheapest and is frankly a slideshow. Only drop below the default if the user asked for something cheaper."),
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 so the brand's own product or saved creator is its subject (the egg rolling off the table becomes your product; your host does the viral move), then join it into your clip so the cut links by identity. 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 confirms their consent, logged), or neither for the brand's first product photo. A DUO OR GROUP (2-4 people at once, e.g. both performers of a duet): subjects [{creator or image, position: 'left'|'right'|'center' or your own words, refs?}], one per person; each is bound to its place in the frame and keeps that performer's movement and timing on the original sound; every real face is checked like a single one. FULL LENGTH IN ONE CALL: past the editor's one pass (15 s with a face) the window is recast in passes with the same references and instruction, cut at the stillest moment, hard-cut joined under the original sound: one job, one video, up to 60 s, quoted as passes plus join; the reply names the cut to watch (no seed: a face can shift slightly there). mode 'auto' (default): 'swap' replaces the hook's subject inside its own scene with a region edit (camera, set, light, timing, on-screen text and sound kept), 'motion' makes your person perform the hook's moves (motion transfer; needs a person in the hook). A face on either side routes to an editor that takes faces (free local check). swap: your own words for what changes ('the egg becomes our serum bottle'; look with video_frames first). start/end pick the window (3 s minimum; default: a duo or group takes the whole clip, one subject the first 10 s). PAID, QUOTED FIRST: without confirm it returns the plan and credits and renders nothing; call again with confirm:true once the user okays it. Returns one job: get_job until done, then post_edit with videoUrl = the recast hook and ops [{op:'join', clips:[{url: your clip}], bridge:{kind:'impact'}}].",
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
- if (a.useBrand !== false && !a.creator && !a.image && !subs.length) { try { b = (await readStore('heist.brand.v1')) || {}; } catch { b = {}; } }
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.358",
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",