hermoso 0.1.226 → 0.1.228

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/README.md CHANGED
@@ -5,7 +5,7 @@ scripts. Research the ads already winning in a market, generate finished image &
5
5
  composited in, copy + CTA included), publish them to your own social channels, and build & manage the ad
6
6
  campaigns behind them — all over [MCP](https://modelcontextprotocol.io) tools, a CLI, or installable Claude skills.
7
7
 
8
- **824 tools.** `tools/list` is always the authoritative set; `hermoso_capabilities` (free) returns the live model
8
+ **825 tools.** `tools/list` is always the authoritative set; `hermoso_capabilities` (free) returns the live model
9
9
  catalog with exact per-render credit costs plus the full capability map.
10
10
 
11
11
  **What it connects to.** Ad platforms: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads,
@@ -171,7 +171,7 @@ block entirely if you signed in above; it is there for CI, where the process can
171
171
 
172
172
  Then ask your agent: *“Generate an image ad with Hermoso.”*
173
173
 
174
- ### What the 824 tools cover
174
+ ### What the 825 tools cover
175
175
 
176
176
  **Ad spy / research** — `find_competitors`, `competitor_teardown`, `pull_competitor_ads`, `research_ads`; the
177
177
  Meta / Google / LinkedIn ad libraries (`search_meta_ads`, `search_google_ads`, `search_linkedin_ads`); organic
package/mcp/tools.mjs CHANGED
@@ -2601,7 +2601,10 @@ function buildTools(rawServer, opts = {}, sink = null) {
2601
2601
  const desc = String(h.description || '');
2602
2602
  let score = 0;
2603
2603
  if (q) {
2604
- const words = q.split(/[\s,]+/).map(expandQueryWord).filter(Boolean);
2604
+ // A NAME-SHAPED ASK IS ALSO ITS WORDS (2026-09-12). Agents search the name they guess (list_meta_campaigns,
2605
+ // update_meta_ad, edit_meta): kept as one literal token it matched nothing and filed a dead end, while its parts
2606
+ // (meta + campaigns, update + meta) name real tools. The literal still scores first when it exists.
2607
+ const words = q.split(/[\s,]+/).flatMap((r) => (r.includes('_') ? [r, ...r.split('_').filter((p) => p.length >= 2)] : [r])).map(expandQueryWord).filter(Boolean);
2605
2608
  const descLc = desc.toLowerCase();
2606
2609
  const nameTokens = name.split('_');
2607
2610
  let nameHits = 0, covered = 0;
@@ -16098,6 +16101,47 @@ function buildTools(rawServer, opts = {}, sink = null) {
16098
16101
  return ok(`Cut ${clips.length} ranked clip${clips.length === 1 ? '' : 's'} [job ${r.jobId}]:\n${lines.join('\n')}${capLine}${trunc}${capNote}`, r);
16099
16102
  }));
16100
16103
 
16104
+ // ADD SUBTITLES TO ANY VIDEO (2026-09-12). A plain comment, not a "── SECTION ──" header: build-docs groups tools by
16105
+ // those headers, and this tool belongs to the section clip_video is in.
16106
+ // Dave: "Do we have functionality to add subtitles to our videos or others? … it should be possible to customize the
16107
+ // style of them as well". Burned subtitles existed only INSIDE clip_video and make_explainer; a finished render, an
16108
+ // upload or someone else's video had no way to get them. The look is the same textStyle vocabulary render_ad speaks.
16109
+ server.registerTool('add_subtitles', {
16110
+ title: 'Add subtitles to a video',
16111
+ description: "Burn subtitles into ANY existing video and get the .srt too. It transcribes the speech and burns short readable lines onto the whole video; nothing is cut or re-rendered. Set textStyle only when the user describes a look; with none, white sentence-case text with a thin outline sits in the bottom safe band. Timing is approximate (per spoken sentence), not word-level sync. burn:false returns only the .srt. Takes a /generated/ URL, a direct .mp4/.mov/.webm, or a YouTube/Vimeo/Loom-style link; not TikTok, Instagram or Facebook. No speech is refused and refunded. Runs in the background and lands in the Library.",
16112
+ inputSchema: {
16113
+ video: z.string().describe('the video to subtitle'),
16114
+ textStyle: z.union([
16115
+ z.enum(['pill', 'editorial', 'bold', 'minimal', 'handwritten', 'boxed']),
16116
+ z.object({
16117
+ preset: z.enum(['pill', 'editorial', 'bold', 'minimal', 'handwritten', 'boxed']).optional(),
16118
+ font: z.enum(['sans', 'serif', 'elegant', 'condensed', 'hand']).optional(),
16119
+ weight: z.number().optional(), size: z.union([z.enum(['s', 'm', 'l', 'xl']), z.number()]).optional(),
16120
+ color: z.string().optional().describe('#hex'), background: z.string().optional().describe('none | pill | #hex box'),
16121
+ position: z.enum(['top', 'center', 'lower', 'bottom']).optional(), textCase: z.enum(['as-is', 'upper', 'lower', 'title']).optional(),
16122
+ italic: z.boolean().optional(), outline: z.boolean().optional(), shadow: z.boolean().optional(),
16123
+ tilt: z.number().optional().describe('degrees, ±12'),
16124
+ }),
16125
+ ]).optional().describe('the look: a preset or overrides; omit for the default'),
16126
+ burn: z.boolean().optional().describe('false = only the .srt'),
16127
+ },
16128
+ outputSchema: { ...JOB_OUT },
16129
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
16130
+ }, wrap(async (a) => {
16131
+ const r = await renderJob('subtitles', { video: a.video, textStyle: a.textStyle, burn: a.burn }, 'MCP subtitles');
16132
+ if (r.stillRendering) return okVideo('', r); // resumable handle — get_job carries the result when it lands
16133
+ const raw = r?.raw || {};
16134
+ // THE HEADLINE IS THE READ-BACK: "burned" only when the returned file really carries the subtitles.
16135
+ const head = raw.captionsBurned && raw.video
16136
+ ? `Subtitles burned in (${raw.captionStyle || 'default'} look, approximate per-sentence timing, not word-level sync): ${abs(raw.video)}`
16137
+ : 'No subtitled video came back — the subtitle file is below.';
16138
+ const note = raw.captionNote ? `\nNOTE: ${raw.captionNote}` : '';
16139
+ const trunc = raw.truncated ? `\nNOTE: only the first ${Math.round((raw.analyzedSeconds || 0) / 60)} min of ${Math.round((raw.sourceDuration || 0) / 60)} min was transcribed, so the subtitles stop there.` : '';
16140
+ const srt = String(raw.srt || '');
16141
+ const file = srt ? `\n\n.srt (${raw.cues || 0} lines):\n${srt.slice(0, 1500)}${srt.length > 1500 ? '\n…' : ''}` : '';
16142
+ return ok(`${head} [job ${r.jobId}]${note}${trunc}${file}`, r);
16143
+ }));
16144
+
16101
16145
  server.registerTool('make_explainer', {
16102
16146
  title: 'Make an explainer video',
16103
16147
  description: "Turn a TOPIC into a finished narrated explainer video. Writes a sectioned script, paints a BURST of pictures per section (about one every 1.5s — most of them one-detail edits of the frame before, so it reads as movement rather than a slideshow), narrates each section with TTS, holds each picture PERFECTLY STILL for its own slice of the narration (the motion is the CUT RATE — a slow move on a still shimmers), then composites the end card (and any on-screen text you asked for) with the Chrome+ffmpeg engine the ads use (text is never model-painted, so it never garbles). BURNED ON-SCREEN TEXT IS OFF BY DEFAULT — the narration carries the point and the pictures carry the story, so the film ships clean unless the user asks otherwise; `captions:true` adds held key points and `subtitles:true` adds narration-timed CAPS (see both). It is an image film WITH motion, not N video-model renders — that's what keeps it affordable. `style` picks the visual family: the default 'cinematic' is photoreal editorial; every other id is a STYLED, strictly non-photoreal look (illustrated / collage / clay / pixel …) that first renders ONE style-key image and then locks every scene to it, so the whole film holds one look. Cost at the default frame density: a ~130-credit hold for a 60s explainer on the default style, ~100 styled; `frameDensity:'lean'` roughly halves it and `'minimal'` (one picture per section) is ~30. All settle to the exact per-frame image + narration spend (a longer target = more sections = more). Takes SEVERAL minutes — one image render per frame; independent frames are painted concurrently, so it is far faster than the frame count suggests. Needs the writing model and a narration voice engine connected. NOT the tool for a short product ad — use render_ad or generate_video for those, and make_template_ad for the deterministic native formats.",
@@ -18336,7 +18380,7 @@ function memoryNoteVerdict(text) {
18336
18380
  residual: z.any().optional().describe('source-branding sweep result ({clean, note})'),
18337
18381
  },
18338
18382
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
18339
- }, cloneStaticHandler);
18383
+ }, (a, extra) => cloneStaticHandler(a, extra)); // its OWN function object: the registry stamps each handler with one tool name, and a shared one ended up named remix_static for both
18340
18384
 
18341
18385
  server.registerTool('mine_angles', {
18342
18386
  title: 'Mine customer angles',
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.226",
3
+ "version": "0.1.228",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
- "description": "AI ad studio and marketing MCP server with 824 tools. Research the ads already running in any market, generate finished image, video and UGC avatar ads, publish and schedule them to your own channels, build and manage the ad campaigns behind them, and read what they achieved. 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.",
5
+ "description": "AI ad studio and marketing MCP server with 825 tools. Research the ads already running in any market, generate finished image, video and UGC avatar ads, publish and schedule them to your own channels, build and manage the ad campaigns behind them, and read what they achieved. 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",
7
7
  "bin": {
8
8
  "hermoso": "bin/hermoso.mjs"