hermoso 0.1.398 → 0.1.401
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 +24 -5
- package/mcp/tools.mjs +19 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Hermoso
|
|
1
|
+
# Hermoso: MCP, CLI & Skills
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Marketing on autopilot**, run from **any AI agent**: Claude Code, Claude.ai, Cursor, Codex, or your own
|
|
4
4
|
scripts. Research the ads already winning in a market, generate finished image & video ads (your real product
|
|
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.
|
|
@@ -8,6 +8,10 @@ campaigns behind them, all over [MCP](https://modelcontextprotocol.io) tools, a
|
|
|
8
8
|
**906 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
|
+
**A Chrome extension too.** [Hermoso: save and clone ads](https://chromewebstore.google.com/detail/hermoso-save-and-clone-ad/kmknkbldeilchhblbfddfnfbadmgcklc)
|
|
12
|
+
saves any ad to your swipefile in one click, clones it for your brand, and pulls any company's ads from its
|
|
13
|
+
website.
|
|
14
|
+
|
|
11
15
|
**Most of it costs nothing.** Publishing and scheduling posts, building and managing paid campaigns, analytics
|
|
12
16
|
and insights, comments and DMs, connectors, profiles and team seats are **free on every plan**, with no
|
|
13
17
|
per-post or per-channel fee and no seat pricing. Credits are spent only on running an AI model (image, video,
|
|
@@ -19,8 +23,8 @@ charges per API request.
|
|
|
19
23
|
Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads, ChatGPT Ads and AppLovin Ads, plus
|
|
20
24
|
product feeds in Google Merchant Center. Publishing and scheduling — **ten** channels: Facebook, Instagram,
|
|
21
25
|
Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky and Telegram. Messaging: WhatsApp (you message a
|
|
22
|
-
person, so it is not an eleventh publishing channel) and
|
|
23
|
-
story reply or an ig.me link gets an automatic
|
|
26
|
+
person, so it is not an eleventh publishing channel) and DM automations on Instagram, Facebook Pages and
|
|
27
|
+
Messenger, and X (a keyword comment, a DM, a story reply or an ig.me / m.me link gets an automatic reply). Ad research: the Meta, Google and LinkedIn ad libraries plus organic TikTok,
|
|
24
28
|
Instagram, YouTube, Threads and Reddit. Analytics: Google Analytics 4, Google Search Console and every
|
|
25
29
|
connected platform's own post and campaign insights. Files: Google Drive, Sheets, Docs and OneDrive.
|
|
26
30
|
|
|
@@ -254,9 +258,18 @@ take** with native synchronized audio. `hermoso_capabilities` is the live list
|
|
|
254
258
|
exact credit cost of every tier — and naming that model in `model` is how you get it, since an unnamed render is
|
|
255
259
|
routed by a narrower auto-pool.
|
|
256
260
|
|
|
261
|
+
**Ads Studio** (`make_static_ads`): pick products and a count and get a batch of finished static ads in one call,
|
|
262
|
+
each a different selling angle and a different layout, with your real product photo and logo laid on. The product
|
|
263
|
+
catalog it draws from (`list_products`, `add_product`, `refresh_products`, `update_product`, `remove_product`) is a
|
|
264
|
+
cache of each product's live page, so names, photos and prices come from the page itself.
|
|
265
|
+
|
|
266
|
+
**Swap a person into any video** (`recast_hook`, `recast_motion`): keep the scene, the cuts and the sound and change
|
|
267
|
+
who is on screen, using your own photo, a saved creator or an AI person (in the web Studio you can describe a presenter in words).
|
|
268
|
+
Real faces from your own photos need a paid plan; AI people work on every plan.
|
|
269
|
+
|
|
257
270
|
**Raw model playground** — the full catalog (30+ image / video / voice / writing models, each with its exact
|
|
258
271
|
per-render credit cost) with no ad framing: `generate_image` / `generate_video` with `useBrand:false`,
|
|
259
|
-
`generate_voice`, `generate_text`.
|
|
272
|
+
`generate_voice`, `generate_text`. The image default is Nano Banana 2.1.
|
|
260
273
|
|
|
261
274
|
**Publish to your own channels** — **ten** of them: Facebook, Instagram and Threads (`post_to_meta`), TikTok
|
|
262
275
|
(`post_to_tiktok`), YouTube (`post_to_youtube` + `update_youtube_video`, `youtube_video_insights`, comments
|
|
@@ -297,6 +310,12 @@ DMed you, and a DM saying STOP opts that person out. Facebook is free like Insta
|
|
|
297
310
|
automation is charged in credits for every mention, reply or DM X delivers plus each reply or DM sent, and running out
|
|
298
311
|
of credits stops its X rules.
|
|
299
312
|
|
|
313
|
+
**Autopilot posting** (`set_post_refill`, `get_post_refill`, `run_post_refill`): say how often, where and what
|
|
314
|
+
about ("2 posts a day on Instagram and Bluesky about our slow-morning routine, let me review first") and Hermoso
|
|
315
|
+
makes new posts from your brand and fills your calendar. Pick review first (each batch waits for your approval,
|
|
316
|
+
edit or redo) or post automatically (straight onto the schedule, still pullable). It follows the notes you leave and
|
|
317
|
+
writes from what has worked on each channel. Credits are used only when a post is made.
|
|
318
|
+
|
|
300
319
|
**Run the ads** — full campaign trees, built paused and read back before anything is reported, with every spend
|
|
301
320
|
change confirm-gated, on **twelve** platforms: **Meta**, **Google Ads**, **LinkedIn Ads**, **Reddit Ads**,
|
|
302
321
|
**Pinterest Ads**, **Microsoft Advertising**, **ChatGPT Ads** (OpenAI's Advertiser API), **X Ads**, **TikTok Ads**,
|
package/mcp/tools.mjs
CHANGED
|
@@ -18342,7 +18342,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
18342
18342
|
title: 'Make template ad',
|
|
18343
18343
|
description: "An ad or post rendered from HTML: no AI model, ~30s, a couple of credits. Presets are SHORTCUTS; 'custom' is YOUR OWN design as config.html (+ css), so no layout, type, colour or motion is 'unsupported'. custom: { html, css?, size? ('9:16' default | '4:5' | '1:1' | '16:9' | any 'W:H' | {w,h} px), durationSeconds? (1-60 = VIDEO; CSS/SVG animation and <video> are frame-stepped, scripts stripped), slides?:[{html, css?}] (2-35 = carousel) }; {{logo}} {{brandName}} {{domain}} {{accent}} fill from the brand; images and fonts load by https URL; notes[] lists what failed to load. YOU author preset copy: short, casual, believable, finished phrases within budget. The preset ids — slideshow, imessage-chat, chatgpt-chat, apple-notes, value-prop, static-mockup, airdrop-carousel, app-ui-tour, imessage-cascade, photo-grid, vignette, kinetic-type, myth-vs-fact, carousel — and each one's fields are listed on `config`. config.music on a VIDEO: omit and the format gets a music bed from our library, matched to its mood and free, whenever the library is stocked (hermoso_capabilities hasMusic); with none on file the video carries only its own sound effects, and the reply says so. 'off' for silence, or any words (a mood or a description) to compose a bed to them (a flat music fee, in hermoso_capabilities). Image URLs may be any public URL.",
|
|
18344
18344
|
inputSchema: {
|
|
18345
|
-
config: z.object({}).passthrough().describe("MUST include config.template: 'custom' or a preset id, plus its fields. PRESETS: 'slideshow' (IMAGES, TikTok photo mode / Reels 1080x1920, or size:'4:5' feed carousels; no branding): { slides:[{text, sub?, image?, blur?, background?, position?}] (2-35; words never rewritten), style? ('tiktok-classic'|'clean-minimal'|'note-style' or a look in words), textStyle?, video?:true (+ an MP4) }; 2 credits, +1 per slide past 5, +2 for the MP4. 'imessage-chat' (VIDEO ~15s): { thread:{contactName, messages:[{from:'them'|'me', text?, product?:{image,title,domain}}]}, theme?, endCard }. 'chatgpt-chat' (VIDEO): { question, answer (may **bold** the brand), productImage?, endCard }. 'apple-notes' (VIDEO): { title, lines[], theme?, endCard }. 'value-prop' (VIDEO ~17s): { hook ≤40ch, claims[3-5 ≤34ch], productImages[2-3], palette[], endCard }. 'static-mockup' (IMAGE): { style:'imessage'|'notes'|'card', size?:{w,h}, ...fields }. 'airdrop-carousel' (VIDEO): { brandName, products:[{image, title?}] (3-16), endCard }. 'app-ui-tour' (VIDEO): { hook?, appName, iconImage?, beats:[{screenImage, caption}] (2-6), endCard }. 'imessage-cascade' (VIDEO): { notifications:[{sender, text}] (4-8), backgroundImage?, endCard }. 'photo-grid' (VIDEO): { title?, photos:[{image, label?}] (4-9), endCard }. 'vignette' (VIDEO): { hook, lines[2-4 ≤40ch], heroImage, endCard }. 'kinetic-type' (VIDEO, own SFX): { phrases[3-6 ≤34ch], productImages?[≤4], endCard }. 'myth-vs-fact' (VIDEO with a real VOICEOVER, small extra charge): { pairs:[{myth ≤50ch, fact ≤60ch}] (2-4; [brackets] accent), endCard }, real truths only. 'carousel' (IMAGES, 5-10 branded 1080x1080): { cover:{hook?, title}, slides:[{headline, support?, stat?:{value, label}}] (3-8), cta:{headline, cta?, domain?}, productImage?, logo? }. endCard = { headline, cta, domain?, logo?, color? }; palette and fontStack optional."),
|
|
18345
|
+
config: z.object({}).passthrough().describe("MUST include config.template: 'custom' or a preset id, plus its fields. PRESETS: 'slideshow' (IMAGES, TikTok photo mode / Reels 1080x1920, or size:'4:5' feed carousels; no branding): { slides:[{text, sub?, image?, blur?, background?, position?}] (2-35; words never rewritten), style? ('tiktok-classic'|'clean-minimal'|'note-style' or a look in words), textStyle?, video?:true (+ an MP4, which takes config.music like any video) }; 2 credits, +1 per slide past 5, +2 for the MP4. 'imessage-chat' (VIDEO ~15s): { thread:{contactName, messages:[{from:'them'|'me', text?, product?:{image,title,domain}}]}, theme?, endCard }. 'chatgpt-chat' (VIDEO): { question, answer (may **bold** the brand), productImage?, endCard }. 'apple-notes' (VIDEO): { title, lines[], theme?, endCard }. 'value-prop' (VIDEO ~17s): { hook ≤40ch, claims[3-5 ≤34ch], productImages[2-3], palette[], endCard }. 'static-mockup' (IMAGE): { style:'imessage'|'notes'|'card', size?:{w,h}, ...fields }. 'airdrop-carousel' (VIDEO): { brandName, products:[{image, title?}] (3-16), endCard }. 'app-ui-tour' (VIDEO): { hook?, appName, iconImage?, beats:[{screenImage, caption}] (2-6), endCard }. 'imessage-cascade' (VIDEO): { notifications:[{sender, text}] (4-8), backgroundImage?, endCard }. 'photo-grid' (VIDEO): { title?, photos:[{image, label?}] (4-9), endCard }. 'vignette' (VIDEO): { hook, lines[2-4 ≤40ch], heroImage, endCard }. 'kinetic-type' (VIDEO, own SFX): { phrases[3-6 ≤34ch], productImages?[≤4], endCard }. 'myth-vs-fact' (VIDEO with a real VOICEOVER, small extra charge): { pairs:[{myth ≤50ch, fact ≤60ch}] (2-4; [brackets] accent), endCard }, real truths only. 'carousel' (IMAGES, 5-10 branded 1080x1080): { cover:{hook?, title}, slides:[{headline, support?, stat?:{value, label}}] (3-8), cta:{headline, cta?, domain?}, productImage?, logo? }. endCard = { headline, cta, domain?, logo?, color? }; palette and fontStack optional."),
|
|
18346
18346
|
},
|
|
18347
18347
|
outputSchema: { ...JOB_OUT },
|
|
18348
18348
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
@@ -18368,7 +18368,9 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
18368
18368
|
const first = await imageBlock(urls[0]).catch(() => null);
|
|
18369
18369
|
const vid = r.raw.video ? `\nVideo version (${Math.round(r.raw.durationSeconds || 0)}s): ${abs(r.raw.video)}` : '';
|
|
18370
18370
|
const notes = Array.isArray(r.raw.notes) && r.raw.notes.length ? `\nNOTE: ${r.raw.notes.join('; ')}` : '';
|
|
18371
|
-
|
|
18371
|
+
// a slideshow MP4 takes config.music like any template video (2026-10-06); what landed is read back, never assumed
|
|
18372
|
+
const mus = r.raw.video && r.raw.music ? (r.raw.music.note ? `\nMusic: ${r.raw.music.note}` : r.raw.music.landed === false ? `\nMusic: no bed landed for "${r.raw.music.mood}" (nothing charged for it).` : `\nMusic: ${r.raw.music.source} bed, "${r.raw.music.mood}".`) : '';
|
|
18373
|
+
return { content: [{ type: 'text', text: `${String(a.config?.template || '') === 'slideshow' ? 'Slideshow' : 'Carousel'} ready — ${urls.length} slides:\n${urls.map((u, i) => ` ${i + 1}. ${u}`).join('\n')}${vid}${mus}${notes} [job ${r.jobId}]` }, ...(first ? [first] : [])], structuredContent: r ?? {} };
|
|
18372
18374
|
}
|
|
18373
18375
|
// notes = what the render could not honour (a custom design's resource that did not load); music = the bed that
|
|
18374
18376
|
// actually landed, or that it did not — both READ BACK off the result, so the agent can judge its own design
|
|
@@ -21186,7 +21188,7 @@ function memoryNoteVerdict(text) {
|
|
|
21186
21188
|
|
|
21187
21189
|
server.registerTool('recast_motion', {
|
|
21188
21190
|
title: 'Recast motion',
|
|
21189
|
-
description: "Motion transfer: re-perform a clip's motion, camera and timing with YOUR character, product, clothes or place; sound kept. image (+ images, @Image1, @Image2… in prompt) = who and what. engine auto (default), a person clip recast with people:
|
|
21191
|
+
description: "Motion transfer: re-perform a clip's motion, camera and timing with YOUR character, product, clothes or place; sound kept. image (+ images, @Image1, @Image2… in prompt) = who and what. engine auto (default), a person clip recast with people: one photo (talking, dance or full-body) or an AI person made from words, ≤15s -> wan (on the beat, keeps set and framing, follows every blink and mouth shape); several photos (one per person) or >15s -> h3 (H3 Max Recast: ≤4 photos, one per person, 5-30s, no shot over 15s, keeps set, light, cuts; engine h3 picks it by name); otherwise seedance (Seedance 2.5, ≤9 pictures, 4-30s, keeps set and beats; a person clip only if our render or faceRoute, else wan/kling). kling = Kling Motion Control (ONE picture, its background becomes the set, 3-30s); wan = Wan 3.0 Prime (≤9 pictures, keeps the set, ≤15s). voice/language: the new performer gets a new voice (same timing) or speaks another language (lip-synced), in the same job and price. Billed per output second, ~5 min per 5s; dryRun quotes.",
|
|
21190
21192
|
inputSchema: {
|
|
21191
21193
|
image: z.string().optional().describe('who performs it: the actor/character image URL (@Image1). Required on kling. No one yet: generate_image a portrait of an AI person from words (no reference photo) and pass its url, or a saved / preset creator’s portrait (list_creators). ' + LIKENESS_TERMS),
|
|
21192
21194
|
images: z.array(z.string()).max(8).optional().describe('seedance/wan: more pictures, @Image2…: a character, product, outfit or place'),
|
|
@@ -21196,14 +21198,21 @@ function memoryNoteVerdict(text) {
|
|
|
21196
21198
|
resolution: z.enum(['480p', '720p', '1080p']).optional().describe('seedance and wan default 1080p; h3 takes 720p or 1080p, default the clip’s own'),
|
|
21197
21199
|
orientation: z.enum(['video', 'image']).optional().describe("kling only: which aspect to keep: the video's (default) or the image's"),
|
|
21198
21200
|
faceRoute: z.enum(['face_lane']).optional().describe(FACE_ROUTE_CLIP_DESC),
|
|
21201
|
+
dryRun: z.boolean().optional().describe('return the credits this exact job would hold and the engine auto picks, without rendering'),
|
|
21202
|
+
voice: z.string().optional().describe("a NEW voice for the new performer: 'match' (a stock voice that fits them), 'female'/'male', or Aria, Sarah, Charlotte, George, Brian, Charlie. Omit = the clip's own sound. Never the original speaker's or a cloned voice"),
|
|
21203
|
+
language: z.string().optional().describe("the new voice speaks this language (e.g. 'Spanish'), lip-synced to the new words; voice defaults to 'match'"),
|
|
21199
21204
|
tier: z.enum(['pro', 'standard']).optional().describe("kling only: 'pro' (default): 1080p and real hand-object interaction. 'standard': about 25% fewer credits and faster, but 720p, and it tends to mime a held object with empty hands. hermoso_capabilities lists the exact credits for both"),
|
|
21200
21205
|
},
|
|
21201
21206
|
outputSchema: { ...JOB_OUT },
|
|
21202
21207
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
21203
|
-
}, wrap(async ({ image, images, video, prompt = '', engine, resolution, orientation = 'video', tier, faceRoute }) => {
|
|
21208
|
+
}, wrap(async ({ image, images, video, prompt = '', engine, resolution, orientation = 'video', tier, faceRoute, dryRun, voice, language }) => {
|
|
21204
21209
|
const more = Array.isArray(images) ? (await Promise.all(images.slice(0, 8).map(toRef))).filter(Boolean) : [];
|
|
21205
21210
|
if (!image && !more.length && !String(prompt || '').trim()) return { content: [{ type: 'text', text: 'Pass image (who performs it), images, or a prompt saying who or what performs the clip.' }], isError: true };
|
|
21206
|
-
const
|
|
21211
|
+
const _in = { ...(image ? { image: await toRef(image) } : {}), ...(more.length ? { images: more } : {}), video, prompt, orientation, engine: engine || 'auto', ...(resolution ? { resolution } : {}), ...(tier ? { tier } : {}), ...(faceRoute === 'face_lane' ? { faceRoute } : {}), ...(voice || language ? { revoice: { voice: voice || 'match', ...(language ? { language } : {}) } } : {}) };
|
|
21212
|
+
// dryRun (2026-10-06): the description has promised "dryRun quotes" while the schema had no such field, so zod dropped
|
|
21213
|
+
// it and an agent asking for a price got a PAID render. Now a quote: the worker's own hold, nothing queued.
|
|
21214
|
+
if (dryRun) { const q = await quoteJob('motion', _in); return ok(`${quoteText(q, 'This motion recast')}${q?.label ? ` Engine: ${String(q.label).replace(/^Motion transfer · /, '')}.` : ''}`, { raw: q }); }
|
|
21215
|
+
const r = await renderJob('motion', _in, 'Motion recast');
|
|
21207
21216
|
const p = renderPayload(r) || {};
|
|
21208
21217
|
return okVideo(`Recast video: ${r.url}${r.model ? ` (${r.model})` : ''}${p.note ? `\n${p.note}` : ''}`, r);
|
|
21209
21218
|
}));
|
|
@@ -21214,7 +21223,7 @@ function memoryNoteVerdict(text) {
|
|
|
21214
21223
|
// the engine; this wrapper only fills the brand's product photo. Held out of the default list on size (ON_DEMAND_TOOLS).
|
|
21215
21224
|
server.registerTool('recast_hook', {
|
|
21216
21225
|
title: 'Recast a viral hook with your subject',
|
|
21217
|
-
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 (
|
|
21226
|
+
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. voice/language: a new voice, or language lip-synced, for one person swapped in. hook: a public post link (TikTok, Reel, Facebook, X, YouTube; one lookup) or a video file. Subject: creator (saved name or id), image (a product or person; a real person's photo records 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 at most (people or product, then location, outfits); past 4, 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 cut to watch. mode 'auto' is always 'swap' (a region edit: camera, set, light, timing, text and sound kept, a person too; a multi-shot hook goes shot by shot); 'motion' only if asked: your person does the moves in their own photo's setting. 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'}}].",
|
|
21218
21227
|
inputSchema: {
|
|
21219
21228
|
hook: z.string().describe('the viral hook: a post link or a video URL'),
|
|
21220
21229
|
creator: z.string().optional().describe("a saved creator's name or id (list_creators). NONE SAVED: cast a preset by exact name (list_creators), or pass image instead: a portrait of an AI person that generate_image painted from words (no reference photo) swaps in on every plan, nothing to save. A described lookalike of the user is fine on every plan; a photo of anyone is a real face (paid plan)"),
|
|
@@ -21229,6 +21238,8 @@ function memoryNoteVerdict(text) {
|
|
|
21229
21238
|
model: z.string().optional().describe('swap: a clip editor id (hermoso_capabilities); omit for the default'),
|
|
21230
21239
|
tier: z.enum(['pro', 'standard']).optional().describe('motion: the motion-transfer tier (default pro)'),
|
|
21231
21240
|
useBrand: z.boolean().optional().describe('false = no brand product photo; true with location/outfit and no subject = also swap in the brand product'),
|
|
21241
|
+
voice: z.string().optional().describe("one person swapped in: a NEW voice for them instead of the original speaker's: 'match' (fits them), 'female'/'male', or Aria, Sarah, Charlotte, George, Brian, Charlie. Omit = keep the clip's voice. Never a cloned voice"),
|
|
21242
|
+
language: z.string().optional().describe("the new voice speaks this language (e.g. 'Spanish'), mouth lip-synced to the new words; voice defaults to 'match'"),
|
|
21232
21243
|
confirm: z.boolean().optional().describe('true = render it (after the user okayed the quote)'),
|
|
21233
21244
|
},
|
|
21234
21245
|
outputSchema: { plan: z.any().optional(), job: z.any().optional(), credits: z.number().optional(), queued: z.boolean().optional() },
|
|
@@ -21239,10 +21250,10 @@ function memoryNoteVerdict(text) {
|
|
|
21239
21250
|
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
|
|
21240
21251
|
if (a.useBrand !== false && !a.creator && !a.image && !subs.length && (!sceneOnly || a.useBrand === true)) { try { b = (await readStore('heist.brand.v1')) || {}; } catch { b = {}; } }
|
|
21241
21252
|
// 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)
|
|
21242
|
-
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 } : {}) };
|
|
21253
|
+
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 } : {}), ...(a.voice ? { voice: a.voice } : {}), ...(a.language ? { language: a.language } : {}), ...(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 } : {}) };
|
|
21243
21254
|
const r = await apiPost('/api/hooks/recast', body);
|
|
21244
21255
|
const p = r?.data || r;
|
|
21245
|
-
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.sourceNote ? `\n${p.sourceNote}` : ''}${p.faceNote ? `\n${p.faceNote}` : ''}${p.instruction ? `\nInstruction: ${p.instruction}` : ''}${p.consent ? `\n${p.consent}` : ''}`;
|
|
21256
|
+
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}.${p.voice ? ` New voice: ${p.voice.line}${p.voice.credits != null ? ` (${p.voice.credits} credits of the total)` : ''}.` : ''}${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.sourceNote ? `\n${p.sourceNote}` : ''}${p.faceNote ? `\n${p.faceNote}` : ''}${p.instruction ? `\nInstruction: ${p.instruction}` : ''}${p.consent ? `\n${p.consent}` : ''}`;
|
|
21246
21257
|
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 } };
|
|
21247
21258
|
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 } };
|
|
21248
21259
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.401",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "Marketing on autopilot, run from your own AI agent. 906 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 and Telegram. 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",
|