hermoso 0.1.444 → 0.1.445

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.
@@ -0,0 +1,101 @@
1
+ // mcp/motion-styles.mjs (byte-twin of cli/mcp/motion-styles.mjs; server.js imports this copy) — THE PRODUCT-SIZZLE LOOKS (2026-10-26, Higgsfield Marketing Studio parity).
2
+ //
3
+ // Higgsfield's Marketing Studio ▸ Video ▸ Motion offers eight product-motion looks (read live 2026-10-26): 2D product
4
+ // motion, Hypermotion, Typography, Dark Minimalism, Color pop, Mixed media, SaaS, Product Showcase. Our product sizzle
5
+ // (the 'hypermotion' job: one paid hero clip diced on its beats, intercut with typeset cards, over a music bed) already
6
+ // IS their Hypermotion; this file adds the other seven as a `style` on the same lane, in our own words. One paid leg,
7
+ // one price, whichever look is picked: a style changes the words sent to the model, never the model, length or tier.
8
+ //
9
+ // PURE: no I/O. tools/motion-styles-check.mjs runs it. Every surface (the job, the Studio tool, both MCP twins, the
10
+ // mini app) reads MOTION_STYLE_IDS from here, so a new look is one entry, never a second hand-written list.
11
+ //
12
+ // WORDS IN THE PICTURE: a video model garbles lettering, so no look asks it to write words. "Typography" is big
13
+ // abstract letterforms and type shapes in the brand colours, never a readable word; the real words ride on the typeset
14
+ // cards, which we render ourselves.
15
+
16
+ export const MOTION_STYLES = Object.freeze({
17
+ hypermotion: {
18
+ label: 'Hypermotion',
19
+ short: 'Fast cuts, crash zooms and speed ramps',
20
+ anchor: 'STYLE ANCHOR: high-energy hypermotion product film — fast rhythmic camera cuts, raw premium sizzle energy, modern and punchy.',
21
+ cinema: 'CINEMATOGRAPHY: cinematic anamorphic lens, deep focus, slight handheld energy, dramatic rim lighting with subtle lens flare, rich color grade.',
22
+ beats: '0-3s crash-zoom from a wide shot to a tight product hero; 3-6s fast lateral orbit around the product; 6-9s slow-motion physics beat (mist, dust or light streaks moving through the air, never on the product surface); 9-12s dynamic pull-back to a wide hero; 12-15s final stillness hero shot',
23
+ allow: '', // the original lane: nothing but the product and atmosphere
24
+ },
25
+ product_2d: {
26
+ label: '2D product motion',
27
+ short: 'The real product with playful hand-drawn 2D animation around it',
28
+ anchor: 'STYLE ANCHOR: mixed 2D/3D product motion — the REAL product, photographed and lit for real, with flat hand-drawn 2D cartoon animation interacting with it: doodled characters, outlined shapes, squiggles and motion lines that hug, pop around and play with the product. Bright, friendly, playful.',
29
+ cinema: 'CINEMATOGRAPHY: clean studio set in one bold flat colour, soft even light, locked or gently drifting camera so the 2D animation reads; crisp outlines, flat fills, smooth 12fps-feeling hand-drawn motion on the 2D layer only.',
30
+ beats: '0-3s the product drops into frame and a hand-drawn character pops out beside it; 3-6s the 2D doodles wrap around and orbit the product; 6-9s a playful interaction (the character taps, lifts or points at the product); 9-12s the doodles burst outward in a confetti of flat shapes; 12-15s settle on a clean product hero with a few doodles framing it',
31
+ allow: 'flat hand-drawn 2D doodles and cartoon characters may move around and beside the product (never covering the label, never redrawing the product itself)',
32
+ },
33
+ typography: {
34
+ label: 'Typography',
35
+ short: 'Product framed by huge bold letterforms in the brand colours',
36
+ anchor: 'STYLE ANCHOR: typographic product film — the product stands in front of giant bold abstract letterforms and type shapes that slide, stack, scale and rotate in the brand colours, graphic-design-poster energy. The letterforms are shapes, NOT words: never write a readable word, name or slogan anywhere in the picture.',
37
+ cinema: 'CINEMATOGRAPHY: flat graphic background planes, hard shadows, punchy saturated two-colour palette, smooth confident camera moves, the product lit crisply and sharply in front of the moving type shapes.',
38
+ beats: '0-3s giant letterforms slam in and the product rises in front of them; 3-6s the type shapes slide past in layers while the camera tracks the product; 6-9s a tight macro on the product as a letterform wipes across the frame; 9-12s the shapes stack and rotate around the product; 12-15s settle on a clean centred product hero against a calm block of colour',
39
+ allow: 'big abstract letterform SHAPES in the brand colours may move behind and around the product (shapes only, no readable words)',
40
+ },
41
+ dark_minimal: {
42
+ label: 'Dark minimalism',
43
+ short: 'Moody, premium, a single hard light on black',
44
+ anchor: 'STYLE ANCHOR: dark minimalist luxury product film — the product alone on a deep black or charcoal set, one precise hard light carving its shape, restrained and premium. Nothing else in frame.',
45
+ cinema: 'CINEMATOGRAPHY: slow, deliberate, perfectly smooth camera moves; low-key lighting with a single key and a thin rim, deep shadows, subtle haze, glossy reflections on a dark surface, muted elegant grade.',
46
+ beats: '0-3s the product emerges from darkness as a slow light sweep reveals its silhouette; 3-6s a slow orbit with the rim light tracing the edge; 6-9s an extreme macro of the material and finish; 9-12s a gentle push-in through soft haze; 12-15s settle on a still, perfectly lit product hero on black',
47
+ allow: '',
48
+ },
49
+ color_pop: {
50
+ label: 'Color pop',
51
+ short: 'Bright saturated colour, glossy, joyful',
52
+ anchor: 'STYLE ANCHOR: colour-pop product film — the product on a vivid saturated background in a bold complementary colour, iridescent and holographic accents, glossy candy-bright energy, joyful and eye-catching.',
53
+ cinema: 'CINEMATOGRAPHY: bright high-key studio light, crisp specular highlights, bouncy snappy camera moves, playful product bounces and spins, saturated pop grade with clean edges.',
54
+ beats: '0-3s the product bounces into frame on a burst of colour; 3-6s a quick spin as the background flips between two bold colours; 6-9s an iridescent light sweep across the product in macro; 9-12s a playful bounce and tilt toward camera; 12-15s settle on a glossy centred product hero on a solid vivid colour',
55
+ allow: 'vivid colour fields and iridescent light accents may surround the product',
56
+ },
57
+ mixed_media: {
58
+ label: 'Mixed media',
59
+ short: 'Editorial collage: paper, cut-outs and texture around the product',
60
+ anchor: 'STYLE ANCHOR: mixed-media editorial collage — the real product combined with paper textures, torn edges, cut-out shapes, halftone dots, tape and print grain, magazine-collage art direction, layered and handmade.',
61
+ cinema: 'CINEMATOGRAPHY: flat-lay and frontal framings, collage layers that slide and stop-motion step into place, warm print grade with visible paper grain, soft natural light on the product.',
62
+ beats: '0-3s collage layers snap into place around the product one by one; 3-6s torn-paper shapes slide across and reveal the product; 6-9s a halftone macro of the product finish; 9-12s cut-out shapes rearrange in stop-motion steps around it; 12-15s settle on a balanced collage composition with the product as the hero',
63
+ allow: 'paper textures, torn edges, cut-out shapes, halftone dots and tape may be layered around the product (never covering the label)',
64
+ },
65
+ saas: {
66
+ label: 'SaaS',
67
+ short: 'For apps and software: floating screens, light trails, particles',
68
+ anchor: 'STYLE ANCHOR: premium tech launch film for an app or software product — the real product screens (from the provided picture) float as glowing panels in a dark 3D space, connected by light trails, particle fields and soft gradient glows, sleek and futuristic. Show the real interface; never invent a different app screen.',
69
+ cinema: 'CINEMATOGRAPHY: smooth gliding camera through 3D space, shallow depth of field, cool gradient lighting with soft bloom, particles drifting, crisp glass and glow edges on the panels.',
70
+ beats: '0-3s a burst of particles resolves into the product screen; 3-6s the camera glides past floating panels linked by light trails; 6-9s a close look at the key screen with a soft light sweep; 9-12s the panels assemble into a neat stack as the particles swirl; 12-15s settle on the main screen centred and glowing',
71
+ allow: "the product's real interface screens may float as glowing panels with light trails and particles (the screens are the product; keep their content as provided, never invent other screens)",
72
+ },
73
+ product_showcase: {
74
+ label: 'Product showcase',
75
+ short: 'Hero commercial: ingredients, splashes and a clean pack shot',
76
+ anchor: 'STYLE ANCHOR: hero product commercial — the product surrounded by what it is made of or what it does (ingredients, fruit, water, ice, fabric, tools), splashes and slow-motion elements flying around it, glossy advertising polish.',
77
+ cinema: 'CINEMATOGRAPHY: high-speed macro and slow motion, bright clean key light with strong rim highlights, rich saturated grade, precise motion-control camera moves.',
78
+ beats: '0-3s the product appears as its ingredients or elements fly in around it; 3-6s a slow-motion splash or burst behind the product; 6-9s an extreme macro of a detail on the product; 9-12s a sweeping motion-control arc around the product and its elements; 12-15s settle on a clean pack shot with a few elements frozen mid-air',
79
+ allow: "the product's own ingredients or elements (fruit, water, ice, fabric, tools that fit what it is) may fly and splash around it, never covering the label",
80
+ },
81
+ });
82
+
83
+ export const MOTION_STYLE_IDS = Object.freeze(Object.keys(MOTION_STYLES));
84
+ export const DEFAULT_MOTION_STYLE = 'hypermotion';
85
+
86
+ // Accepts an id or a label in any case ("Dark Minimalism", "dark-minimal", "color pop"); anything else → the default.
87
+ export function resolveMotionStyle(v) {
88
+ const s = String(v ?? '').trim().toLowerCase();
89
+ if (!s) return DEFAULT_MOTION_STYLE;
90
+ const key = s.replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
91
+ if (MOTION_STYLES[key]) return key;
92
+ for (const [id, st] of Object.entries(MOTION_STYLES)) {
93
+ const lab = st.label.toLowerCase().replace(/[^a-z0-9]+/g, '_');
94
+ if (lab === key || lab.replace(/_/g, '') === key.replace(/_/g, '')) return id;
95
+ }
96
+ const alias = { dark_minimalism: 'dark_minimal', minimal: 'dark_minimal', colour_pop: 'color_pop', pop: 'color_pop', collage: 'mixed_media', '2d': 'product_2d', cartoon: 'product_2d', showcase: 'product_showcase', software: 'saas', app: 'saas', type: 'typography', kinetic: 'hypermotion', sizzle: 'hypermotion' }[key];
97
+ return alias || DEFAULT_MOTION_STYLE;
98
+ }
99
+
100
+ // One line per look, for tool descriptions and menus: "hypermotion (Fast cuts…), product_2d (…)…".
101
+ export const motionStyleMenu = () => MOTION_STYLE_IDS.map((id) => `${id} (${MOTION_STYLES[id].short})`).join(', ');
package/mcp/tools.mjs CHANGED
@@ -15,6 +15,7 @@ import { wellFormedValue, wellFormedString } from './well-formed.mjs';
15
15
  // WHICH CONNECTOR A TOOL NEEDS — the same table and the same decision the Studio chat applies (lib/studio-roster.mjs
16
16
  // re-exports every symbol from here). `./roster-scope.mjs` is the only specifier that resolves in a byte-identical
17
17
  // twin, for the same reason ./well-formed.mjs is. See applyToolGates() for the seam and roster-scope.mjs for the law.
18
+ import { MOTION_STYLE_IDS, motionStyleMenu } from './motion-styles.mjs';
18
19
  import { toolHeldBackByConnectors as heldBackByConnectorsOf, toolProvider as providerOf, toolUnoffered, toolWithheldByServer as withheldByServerOf, metaAlternativeNote as metaAlternativeNoteOf } from './roster-scope.mjs';
19
20
  // A tool a widget host lists under ANOTHER name (a WIDGET_HOST_RENAMES alias, or one operation of a WIDGET_HOST_SPLITS
20
21
  // tool) is gated, attributed and explained as the tool it comes from: same connector, same Instagram-Login exemption,
@@ -18902,7 +18903,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
18902
18903
  // locked as the product photo, and a UGC-style video plan. Stops at the plan like clone_video: render_ad is the spend.
18903
18904
  server.registerTool('click_to_ad', {
18904
18905
  title: 'Make a video ad from a product link',
18905
- description: 'Paste a product page link (Shopify, Amazon or any store page) and get a creator-style video ad planned for that product. Hermoso reads the page (name, photos, what it says about the product, price), saves the product to the brand\'s catalog, checks the page\'s real product photo and locks it as the photo the ad is built on, then plans the ad. Nothing renders here: show the user the concept, then call render_ad with the returned creative (dryRun:true first for the exact price). Works with or without a saved brand.',
18906
+ description: 'Paste a product page link (Shopify, Amazon or any store page) and get a creator-style video ad planned for that product. Hermoso reads the page (name, photos, what it says about the product, price), saves the product to the brand\'s catalog, checks the page\'s real product photo and pins it to this ad (the brand\'s saved default photo is not changed), then plans the ad. A store that hides its page from servers is read in a real browser; failing that the name comes from the link and the photo from a search, and the reply says so. Nothing renders here: show the user the concept, then call render_ad with the returned creative (dryRun:true first for the exact price). Works with or without a saved brand.',
18906
18907
  inputSchema: {
18907
18908
  url: z.string().describe('the product page link, https://…'),
18908
18909
  style: z.enum(['ugc_selfie', 'ugc_unboxing', 'ugc_demo', 'ugc_tryon', 'ugc_product', 'ugc_shopping', 'ugc_delivery', 'transformation', 'day_in_life', 'grwm', 'ugc_addiction']).optional().describe('ugc_selfie (talking to camera, default), ugc_unboxing, ugc_demo (how-to), ugc_tryon (try-on or haul), ugc_product (product close-ups, no face), ugc_shopping (in-store find), ugc_delivery, transformation (before and after), day_in_life (at home), grwm (get ready with me), ugc_addiction (obsessed with it)'),
@@ -18927,7 +18928,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
18927
18928
  const c = s?.data?.creative || s?.creative || s;
18928
18929
  const p = d.product || {};
18929
18930
  const planned = Math.round(+c.render_plan?.duration_seconds || (c.video_storyboard?.scenes || []).reduce((n, x) => n + (+x.seconds || 0), 0) || 0);
18930
- const photoLine = d.photo ? `Product photo: the page's own photo, checked and locked (${d.photo.url}).` : `⚠ No product photo was locked: ${d.photoNote || 'no page image passed the check'}. Tell the user; render_ad will ask for one (set_product_image).`;
18931
+ const photoLine = d.photo ? `Product photo: ${d.product?.readFrom === 'search' ? 'found by searching for the product (the store hid its page): check it is the right product' : 'the page\'s own photo'}, checked and pinned to this ad (${d.photo.url}). ${d.photo.savedToBrand ? 'The brand had no product photo, so it is also saved as the brand\'s default.' : 'The brand\'s saved default photo is unchanged.'}` : `⚠ No product photo was locked: ${d.photoNote || 'no page image passed the check'}. Tell the user; render_ad will ask for one (set_product_image).`;
18931
18932
  const text = `Read "${p.title || 'the product'}"${p.host ? ` from ${p.host}` : ''}${p.priceLabel ? ` (${p.priceLabel})` : ''}, saved to the brand's products.\n${photoLine}\nPlan: "${c.concept || ''}"\nLength: ${planned || '—'}s\nHeadline: ${c.copy?.[0]?.headline || ''}\nNext: show the user this concept, then call render_ad with this creative (dryRun:true first for the exact price).`;
18932
18933
  return ok(text, { ...c, product: p, photo: d.photo || null });
18933
18934
  }));
@@ -19771,6 +19772,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
19771
19772
  cta: z.string().optional().describe('closing CTA line, ≤30 chars'),
19772
19773
  brandName: z.string().optional().describe('brand name on the cards — defaults to the workspace brand'),
19773
19774
  musicMood: z.string().optional().describe('music-bed mood, e.g. driving / cinematic / upbeat'),
19775
+ style: z.enum(MOTION_STYLE_IDS).optional().describe('the LOOK of the sizzle (same price for every look): ' + motionStyleMenu() + '. Default hypermotion.'),
19774
19776
  },
19775
19777
  outputSchema: { ...JOB_OUT },
19776
19778
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
@@ -19778,7 +19780,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
19778
19780
  let b = await readStore('heist.brand.v1'); if (!b || typeof b !== 'object') b = {}; // the cards want the REAL palette/logo, same as post_edit
19779
19781
  const pal = (Array.isArray(b.palette) ? b.palette : []).filter(c => /^#[0-9a-f]{6}$/i.test(String(c || '')));
19780
19782
  const r = await renderJob('hypermotion', {
19781
- prompt: a.prompt, seconds: a.seconds, refImage: a.refImage, aspectRatio: a.aspectRatio, resolution: a.resolution, musicMood: a.musicMood,
19783
+ prompt: a.prompt, seconds: a.seconds, refImage: a.refImage, aspectRatio: a.aspectRatio, resolution: a.resolution, musicMood: a.musicMood, style: a.style,
19782
19784
  cardCopy: { specs: a.specs, cta: a.cta || b.cta || '' },
19783
19785
  brand: { name: a.brandName || b.name || '', domain: b.domain || '', logo: b.logo || '', palette: pal },
19784
19786
  }, 'MCP product sizzle');
@@ -22200,7 +22202,7 @@ function memoryNoteVerdict(text) {
22200
22202
 
22201
22203
  server.registerTool('edit_video', {
22202
22204
  title: 'Edit a video clip',
22203
- description: "EDIT an existing clip from a plain instruction (video-to-video): the motion, timing, framing and cut stay, the named thing changes. 'change only the mug to red', 'make it nighttime', 'restyle it as claymation'. Your words are wrapped so the model keeps everything else identical, changes only what you named and repeats that lock; lighting is kept unless the change needs new light (set lighting to force either); literal:true sends your words as written. One change per call holds best. previewFirstFrame:true edits ONE still first (one image edit; previewAt picks the second) and quotes the clip; nothing else runs until you call again, ideally with previewStill. The reply scores how well the shot held outside the change (free) and flags an edit that touched more than asked. NOT for cuts/trims/end cards (post_edit), a new video (generate_video / render_ad), translation (dub_video) or a saved creator's face (recast_motion). Best on 3-15s clips.",
22205
+ description: "SWAP AN OBJECT IN A VIDEO ('replace the cup with our can' + the product photo as elements) or EDIT an existing clip from a plain instruction (video-to-video): the motion, timing, framing and cut stay, the named thing changes. 'change only the mug to red', 'make it nighttime', 'restyle it as claymation'. Your words are wrapped so the model keeps everything else identical, changes only what you named and repeats that lock; lighting is kept unless the change needs new light (set lighting to force either); literal:true sends your words as written. One change per call holds best. previewFirstFrame:true edits ONE still first (one image edit; previewAt picks the second) and quotes the clip; nothing else runs until you call again, ideally with previewStill. The reply scores how well the shot held outside the change (free) and flags an edit that touched more than asked. NOT for cuts/trims/end cards (post_edit), a new video (generate_video / render_ad), translation (dub_video) or a saved creator's face (recast_motion). Best on 3-15s clips.",
22204
22206
  inputSchema: {
22205
22207
  video: z.string().describe('the source video URL (a render, job result or list_library)'),
22206
22208
  instruction: z.string().describe('the change, in the user’s own words'),
@@ -22354,7 +22356,7 @@ function memoryNoteVerdict(text) {
22354
22356
 
22355
22357
  server.registerTool('recast_motion', {
22356
22358
  title: 'Recast motion',
22357
- 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, recommended (≤9 pictures, keeps the set, ≤15s); wan-prime = Wan 3.0 Prime by name (costs more). 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.",
22359
+ description: "Copy the motion of any video (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, recommended (≤9 pictures, keeps the set, ≤15s); wan-prime = Wan 3.0 Prime by name (costs more). 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.",
22358
22360
  inputSchema: {
22359
22361
  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),
22360
22362
  images: z.array(z.string()).max(8).optional().describe('seedance/wan: more pictures, @Image2…: a character, product, outfit or place'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.444",
3
+ "version": "0.1.445",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
5
  "description": "AI marketing agent and AI ad generator, run from your own AI agent. 932 tools. No markup on models. Publishing, scheduling, ad campaign management, comments, DMs and analytics are free on every plan. 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, Telegram and Google Business Profile. 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",