hermoso 0.1.355 → 0.1.358

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 +12 -6
  2. package/package.json +1 -1
package/mcp/tools.mjs CHANGED
@@ -16988,7 +16988,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
16988
16988
  brand: z.union([z.string(), z.object({}).passthrough()]).optional().describe('brand name, or a brand profile object {name,domain,category,palette,products,…}. OMIT to use the workspace’s SAVED brand + memory automatically (see get_brand); use draft_brand to onboard a new one'),
16989
16989
  product: z.string().describe('what to advertise + any angle/offer the user specified'),
16990
16990
  format: z.enum(['auto', 'image', 'video']).optional().describe("'image', 'video', or 'auto' when unspecified"),
16991
- durationSeconds: z.number().optional().describe('VIDEO ONLY — the total spot length the user explicitly asked for, in seconds, copied verbatim (30 for "a 30 second ad"). The planner authors the storyboard TO it: the scenes’ seconds sum to it and the script is word-budgeted for it. Supported range 4–180; anything outside is CLAMPED to it (the reply says so). A length that fits ONE clip of the render model renders as a single continuous pass; anything longer is STITCHED from acts filled to that model’s clip maximum with the remainder last (on a 15s-clip model, 40 -> 15+15+10 and 17 -> 13+4) — never time-compressed. The maximum is the model’s own: 15s on most, 30s on the longest-clip model, so a 30s ad can be one unbroken take rather than two acts. Omit when the user named no length; do NOT pass a guess, an omitted value keeps the recipe-aware default.'),
16991
+ durationSeconds: z.number().optional().describe('VIDEO ONLY — the total spot length the user explicitly asked for, in seconds, copied verbatim (30 for "a 30 second ad"). The planner authors the storyboard TO it: the scenes’ seconds sum to it and the script is word-budgeted for it. Supported range 4–180; anything outside is CLAMPED to it (the reply says so). A length that fits ONE clip of the render model renders as a single continuous pass; anything longer is STITCHED from acts filled to that model’s clip maximum with the remainder last (on a 15s-clip model, 40 -> 15+15+10 and 17 -> 13+4) — never time-compressed. The maximum is the model’s own: 15s on most, 30s on the longest-clip model, so a 30s ad can be one unbroken take rather than two acts. Omit when the user named no length; do NOT pass a guess: an omitted value is the default, a 30s spot in one unbroken take (2026-09-29), and a length the user names always wins.'),
16992
16992
  hook: z.string().optional().describe('force the VISUAL scroll-stop mechanic the opening beat is built on — a hook id from list_hooks (e.g. "direct_callout", "mid_problem", "macro_asmr"). Omit to let the planner pick. A hook that cannot be delivered in this brief is DROPPED with the reason rather than rendered wrongly — an on-screen-text hook on an authentic/UGC ad is the one that bites, because that register carries zero on-screen text.'),
16993
16993
  setting: z.string().optional().describe('force the WHERE — a setting id from list_hooks (e.g. "kitchen", "gym", or a surreal one like "volcano_rim" / "airplane_wing", which are played 100% straight and never acknowledged). Omit for a neutral setting.'),
16994
16994
  recipe: z.string().optional().describe('a recipe id from hermoso_capabilities to force an archetype'),
@@ -19878,6 +19878,10 @@ function memoryNoteVerdict(text) {
19878
19878
  const site = await apiGet('/api/site/images', { url: p.domain });
19879
19879
  const imgs = [...(site?.images || [])].filter(Boolean);
19880
19880
  if (imgs.length) { p.productImages = imgs.slice(0, 12); if (!p.product) p.product = imgs[0]; }
19881
+ // THE PRODUCTS' OWN NAMES beside their photos (2026-09-29): the shop's title, else the name the photo review gave
19882
+ // the photo it kept. The Studio's Elements names each product card (and its @handle) from these, never from a
19883
+ // stored file's random name. Only for photos this brand keeps.
19884
+ if (imgs.length) { const _pt = (Array.isArray(site?.productTitles) ? site.productTitles : []).filter((x) => x && typeof x.title === 'string' && x.title.trim() && p.productImages.includes(x.image)).map((x) => ({ image: x.image, title: x.title.trim().slice(0, 60) })); if (_pt.length) p.productTitles = _pt; }
19881
19885
  // products is an array of product-NAME STRINGS everywhere else (public/app.js writes `_prodNames`), and
19882
19886
  // brandContext joins it straight into "use these EXACT names". /api/site/images returns {title,image}
19883
19887
  // OBJECTS, so storing them raw printed "[object Object], [object Object]" into every subsequent plan —
@@ -20116,10 +20120,11 @@ function memoryNoteVerdict(text) {
20116
20120
  // motion and the ORIGINAL AUDIO untouched. The plan is one server call; every variant is a `multiply` job (opening-frame keyframe edit → motion transfer driven by the source, so length, cut, motion and audio are the original's).
20117
20121
  server.registerTool('multiply_ad', {
20118
20122
  title: 'Multiply an ad',
20119
- description: "MULTIPLY a winning video ad into N variants: each gets a NEW character, outfit, location and/or objects while the cut, the camera motion, the pacing and the ORIGINAL AUDIO stay exactly as they were (that is what made the ad work), and any burned-in captions are removed. Pass the source video URL (a previous render, a job result, list_library, or the top performer from post_performance / meta_insights). HOW: the source video itself DRIVES each variant (motion transfer from one image-edited opening frame), so every variant comes back the SAME LENGTH as the source with the same cut, the same performance and the original audio — only the person, outfit, set and props change. Sources up to 30 seconds work as they are; longer ones are refused for free with the way out (trim it first: post_edit with ops [{op:'trim', start:0, end:30}] — clip_video is the AI highlight clipper, not a trim). Returns the plan and ONE JOB PER VARIANT — call get_job on each until it reports done; do not describe a variant before its URL arrives. Cost is quoted per variant in the reply (use dryRun:true to see the plan and the quote without rendering). Regions: pass regions:['Berlin','Tokyo'] to restyle variants per market; translation is a separate, explicit step — dub_video on a finished variant.",
20123
+ description: "MULTIPLY a winning video ad into N variants: each gets a NEW character, outfit, location and/or objects while the cut, the camera motion, the pacing and the ORIGINAL AUDIO stay exactly as they were (that is what made the ad work), and any burned-in captions are removed. Pass the source video URL (a previous render, a job result, list_library, or the top performer from post_performance / meta_insights). HOW: the source video itself DRIVES each variant (motion transfer from one image-edited opening frame), so every variant comes back the SAME LENGTH as the source with the same cut, the same performance and the original audio — only the person, outfit, set and props change. Sources up to 30 seconds work as they are; longer ones are refused for free with the way out (trim it first: post_edit with ops [{op:'trim', start:0, end:30}] — clip_video is the AI highlight clipper, not a trim). Returns the plan and ONE JOB PER VARIANT — call get_job on each until it reports done; do not describe a variant before its URL arrives. Cost is quoted per variant in the reply (use dryRun:true to see the plan and the quote without rendering). Regions: pass regions:['Berlin','Tokyo'] to restyle variants per market; translation is a separate, explicit step — dub_video on a finished variant. `change` is what should change, in the user's own words ('older women', 'winter streets', 'swap the mug for our bottle'): every variant applies it and the variants still differ around it; a part of it about the voice, words, language, music, length or captions cannot change here, and the reply says so and names the tool that can.",
20120
20124
  inputSchema: {
20121
20125
  video: z.string().describe('the source video URL'),
20122
20126
  count: z.number().optional().describe('how many variants, 1-12 (default 6)'),
20127
+ change: z.string().optional().describe("what should change, in the user's own words, e.g. 'older women, winter streets'; every variant applies it (omit to let the variants vary freely)"),
20123
20128
  axes: z.array(z.string()).optional().describe('what to vary: character, outfit, location, objects (default all four), or your own, e.g. "season"'),
20124
20129
  notes: z.string().optional().describe('anything the variants must respect, e.g. "keep it women 25-40", "no gyms"'),
20125
20130
  regions: z.array(z.string()).optional().describe('markets to restyle for, one or more variants each, e.g. ["Berlin","Tokyo","São Paulo"] — visuals only; audio is never translated here'),
@@ -20127,21 +20132,22 @@ function memoryNoteVerdict(text) {
20127
20132
  },
20128
20133
  outputSchema: { jobs: z.array(z.any()).optional(), plan: z.any().optional(), perVariantCredits: z.number().optional(), totalCredits: z.number().optional(), dryRun: z.boolean().optional() },
20129
20134
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
20130
- }, wrap(async ({ video, count, axes, notes, regions, dryRun }) => {
20135
+ }, wrap(async ({ video, count, change, axes, notes, regions, dryRun }) => {
20131
20136
  const src = String(video || '').trim();
20132
20137
  if (!/^https?:\/\//.test(src)) return { content: [{ type: 'text', text: 'Pass the source video as a URL — a previous render, a job result, or an entry from list_library.' }], isError: true };
20133
20138
  const n = Math.max(1, Math.min(12, Math.round(+count) || 6));
20134
- const plan = await apiPost('/api/multiply/plan', { video: src, count: n, ...(axes ? { axes } : {}), ...(notes ? { notes } : {}), ...(regions ? { regions } : {}) });
20139
+ const plan = await apiPost('/api/multiply/plan', { video: src, count: n, ...(change ? { change } : {}), ...(axes ? { axes } : {}), ...(notes ? { notes } : {}), ...(regions ? { regions } : {}) });
20135
20140
  const p = plan?.data || plan;
20141
+ const changeLine = `${p.change ? `\nEvery variant: ${p.change}` : ''}${p.changeNote ? `\n${p.changeNote}` : ''}`; // WHAT SHOULD CHANGE (2026-09-29), and what of it a variant cannot change
20136
20142
  const lines = (p.variants || []).map((v, i) => `${i + 1}. ${v.label} — ${['character', 'outfit', 'location', 'objects'].filter(a => v[a] && v[a] !== 'same').map(a => a + ': ' + v[a]).join('; ')}`);
20137
20143
  const quote = `~${p.perVariantCredits} credits per variant · ~${p.totalCredits} for ${(p.variants || []).length}`;
20138
- if (dryRun) return { content: [{ type: 'text', text: `Plan (nothing rendered). Source: ${Object.entries(p.source || {}).map(([k, v]) => k + ': ' + v).join('; ')}\n${lines.join('\n')}\n${quote}. Run again without dryRun to render.` }], structuredContent: { plan: p, perVariantCredits: p.perVariantCredits, totalCredits: p.totalCredits, dryRun: true } };
20144
+ if (dryRun) return { content: [{ type: 'text', text: `Plan (nothing rendered). Source: ${Object.entries(p.source || {}).map(([k, v]) => k + ': ' + v).join('; ')}\n${lines.join('\n')}${changeLine}\n${quote}. Run again without dryRun to render.` }], structuredContent: { plan: p, perVariantCredits: p.perVariantCredits, totalCredits: p.totalCredits, dryRun: true } };
20139
20145
  const jobs = [];
20140
20146
  for (const v of (p.variants || [])) {
20141
20147
  const job = await submitJob('multiply', { video: src, instruction: v.instruction, label: v.label }, { label: `Multiply · ${v.label}` });
20142
20148
  jobs.push({ id: job.id, label: v.label });
20143
20149
  }
20144
- return { content: [{ type: 'text', text: `Multiplying — ${jobs.length} variant(s) queued, original audio kept on all of them. ${quote}.\n${jobs.map((j, i) => `${i + 1}. ${j.label} → job ${j.id}`).join('\n')}\nEach variant is one keyframe edit plus a motion transfer of the whole source (2-6 minutes) and comes back the SAME length as the source. Call get_job with each id until it reports done; a variant has NO file until then.` }], structuredContent: { jobs, plan: p, perVariantCredits: p.perVariantCredits, totalCredits: p.totalCredits } };
20150
+ return { content: [{ type: 'text', text: `Multiplying — ${jobs.length} variant(s) queued, original audio kept on all of them. ${quote}.${changeLine}\n${jobs.map((j, i) => `${i + 1}. ${j.label} → job ${j.id}`).join('\n')}\nEach variant is one keyframe edit plus a motion transfer of the whole source (2-6 minutes) and comes back the SAME length as the source. Call get_job with each id until it reports done; a variant has NO file until then.` }], structuredContent: { jobs, plan: p, perVariantCredits: p.perVariantCredits, totalCredits: p.totalCredits } };
20145
20151
  }));
20146
20152
 
20147
20153
  // HOOK MULTIPLIER (2026-09-18): ONE finished video ad → N complete versions with NEW opening hooks, the rest of the ad and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.355",
3
+ "version": "0.1.358",
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",