hermoso 0.1.491 → 0.1.493
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/mcp/tools.mjs +23 -2
- package/package.json +1 -1
package/mcp/tools.mjs
CHANGED
|
@@ -21983,7 +21983,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
21983
21983
|
};
|
|
21984
21984
|
server.registerTool('make_motion_film', {
|
|
21985
21985
|
title: 'Make a motion graphics video',
|
|
21986
|
-
description: "MOTION GRAPHICS and ANIMATED VIDEO in one call
|
|
21986
|
+
description: "MOTION GRAPHICS and ANIMATED VIDEO in one call: kinetic typography (animated text), a logo animation or intro, a product launch film, an animated social cut. MOVING PICTURES are the default for launch, product and social: in each scene with a picture, one thing physically happens (a seed splits open, an eye opens, the view travels through a forest), filmed by an image-to-video model, with the type, transitions and the music's beat put on top; a few minutes, priced per shot, and with no pictures given each scene paints its own. moving:false, and always for type and logo, makes it in code with no video model: type, pictures and drawn graphics filmed by a moving virtual camera, a 1080p MP4 in about a minute, billed for its compute only. Pick it for 'animated video', 'motion graphics', 'kinetic text', 'text animation', 'logo animation', 'logo intro', 'animated explainer' asks. recipe: 'type' (every line its own animated card, no picture needed), 'logo' (the logo assembles out of dots and settles, the name and tagline land under it), 'launch' (30s 16:9), 'product' (15s), 'social' (15s 9:16). Or `spec`, the whole film as JSON (help:true returns the language). STEPS: without confirm it is a FREE quote that gives both prices (length, the music's tempo; the spec in structuredContent): say both; preview:true is a 1-credit contact sheet of the still version; confirm:true renders. You may write `moves` yourself, or the film's own planner writes them for a few credits. Only the user's or the brand's words appear: never invent a claim, number or review. projectUrl = a past film's url returns its saved spec to edit and render again.",
|
|
21987
21987
|
inputSchema: {
|
|
21988
21988
|
recipe: z.enum(['type', 'logo', 'launch', 'product', 'social']).optional().describe('type, logo, launch, product or social; help:true lists the recipes'),
|
|
21989
21989
|
hook: z.string().optional().describe("opening line; '*word*' = italic accent"),
|
|
@@ -21998,6 +21998,11 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
21998
21998
|
music: z.string().optional().describe("'off' = silent"),
|
|
21999
21999
|
theme: z.enum(['light', 'dark']).optional().describe("logo: dark for a light logo"),
|
|
22000
22000
|
energy: z.number().optional().describe('how much the film keeps moving, 0 to 1; 0 films exactly what is written'),
|
|
22001
|
+
moving: z.boolean().optional().describe('true: one thing physically happens in each picture, filmed by a video model. false: stills moved by a virtual camera, far cheaper. Default true for launch, product, social'),
|
|
22002
|
+
moves: z.array(z.object({ scene: z.string().describe('scene id'), action: z.string().describe('the one thing that physically happens, in plain words'), still: z.string().optional().describe('a picture to animate, when not the one in the scene'), stillPrompt: z.string().optional().describe('a picture to paint first, for a scene with none'), layer: z.string().optional().describe("the picture layer's id, when the scene has several"), video: z.string().optional().describe('a shot already filmed: kept, and not paid for again'), matchNext: z.boolean().optional().describe("the shot ends on the next scene's picture") })).optional().describe('per scene, what happens in its picture; omit and the planner writes them'),
|
|
22003
|
+
brief: z.string().optional().describe('what the film is about, for the planner'),
|
|
22004
|
+
model: z.string().optional().describe('image-to-video model id from hermoso_capabilities; default is the best value for short shots'),
|
|
22005
|
+
resolution: z.string().optional().describe('the moving shots: 720p or 1080p (default) where the model has them'),
|
|
22001
22006
|
spec: z.object({}).passthrough().optional().describe('the whole film as JSON, from a quote or a saved film; help:true shows the spec language'),
|
|
22002
22007
|
projectUrl: z.string().optional().describe("a past film's url; returns its saved spec to edit and render again"),
|
|
22003
22008
|
help: z.boolean().optional().describe('true returns the spec language and recipes; renders nothing'),
|
|
@@ -22023,15 +22028,31 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
22023
22028
|
const name = String(a.name || b.name || '').slice(0, 40), brandLogo = logo || b.logo || '';
|
|
22024
22029
|
let spec = a.spec && typeof a.spec === 'object' ? a.spec : null, music = typeof a.music === 'string' && a.music.trim() ? a.music.trim().slice(0, 300) : '';
|
|
22025
22030
|
let credits = null, previewCredits = null, bpm = null;
|
|
22031
|
+
// the moving version is the default wherever a film has pictures to move; a spec already holding filmed shots stays as it is unless asked, so changing a word does not re-buy them
|
|
22032
|
+
const hasClips = Array.isArray(spec?.scenes) && spec.scenes.some((s) => Array.isArray(s?.layers) && s.layers.some((l) => l && l.type === 'clip'));
|
|
22033
|
+
const wantMoving = a.moving ?? (a.preview ? false : Array.isArray(a.moves) && a.moves.length ? true : spec ? !hasClips : a.recipe === 'launch' || a.recipe === 'product' || a.recipe === 'social');
|
|
22026
22034
|
if (!spec) {
|
|
22027
22035
|
if (!a.recipe) throw Object.assign(new Error("Pass a recipe ('type', 'logo', 'launch', 'product' or 'social') with words, or a whole spec."), { status: 400, _userInput: true });
|
|
22028
|
-
const d = await apiPost('/api/motion/build', { recipe: a.recipe, size: a.size || null, seconds: a.seconds ?? null, mood: a.mood || null, theme: a.theme || null, ...(a.energy != null ? { energy: a.energy } : {}), brand: { name, palette: pal, logo: brandLogo ? 'yes' : '' }, words: { hook: a.hook || '', lines: a.lines || [], tagline: a.tagline || '' }, images });
|
|
22036
|
+
const d = await apiPost('/api/motion/build', { ...(wantMoving ? { moving: true } : {}), recipe: a.recipe, size: a.size || null, seconds: a.seconds ?? null, mood: a.mood || null, theme: a.theme || null, ...(a.energy != null ? { energy: a.energy } : {}), brand: { name, palette: pal, logo: brandLogo ? 'yes' : '' }, words: { hook: a.hook || '', lines: a.lines || [], tagline: a.tagline || '' }, images });
|
|
22029
22037
|
spec = d.spec; if (!music) music = d.music || ''; credits = d.credits; previewCredits = d.previewCredits; bpm = d.bpm;
|
|
22030
22038
|
}
|
|
22031
22039
|
const film = { config: { template: 'motion', spec, ...(music ? { music } : {}) }, brand: { name, domain: b.domain || '', logo: brandLogo, palette: pal } };
|
|
22032
22040
|
if (credits == null) { const q = await quoteJob('templatead', film); credits = q?.credits ?? null; if (q?.refused) throw Object.assign(new Error(`The film was refused before anything rendered (free): ${q.refused}`), { status: 400, _userInput: true }); }
|
|
22033
22041
|
const secs = Array.isArray(spec?.scenes) ? +spec.scenes.reduce((x, s) => x + (+s.seconds || 0), 0).toFixed(2) : null;
|
|
22034
22042
|
const what = `${secs != null ? secs + 's, ' : ''}${spec?.size || '16:9'}, ${Array.isArray(spec?.scenes) ? spec.scenes.length : '?'} scenes${music && music !== 'off' ? `, music "${music}"${bpm ? ` at ${bpm} BPM` : ''}` : ''}`;
|
|
22043
|
+
if (wantMoving) {
|
|
22044
|
+
const mv = { ...(a.model ? { model: a.model } : {}), ...(a.resolution ? { resolution: a.resolution } : {}) };
|
|
22045
|
+
const q = await apiPost('/api/motion/moving-quote', { spec, music, ...(Array.isArray(a.moves) && a.moves.length ? { moves: a.moves } : {}), ...mv });
|
|
22046
|
+
if (q?.moving) {
|
|
22047
|
+
if (!a.confirm) return ok(`QUOTE (nothing rendered or charged): with moving pictures this film is about ${q.credits} credits (${what}; ${q.moving} moving shot${q.moving === 1 ? '' : 's'} on ${q.model} at ${q.resolution}). With still pictures moved by a virtual camera it is ${q.stillFilmCredits} credits (moving:false). ${PRICE_ESTIMATE_LINE} Tell the user both prices, then call again with the same arguments and confirm:true (pass structuredContent.spec as spec to keep this exact film).`, { credits: q.credits, stillFilmCredits: q.stillFilmCredits, moving: q.moving, shots: q.scenes, lines: q.lines, model: q.model, resolution: q.resolution, seconds: secs, music, bpm, spec });
|
|
22048
|
+
let moves = Array.isArray(a.moves) && a.moves.length ? a.moves : null, planned = 0;
|
|
22049
|
+
if (!moves) { const p = await apiPost('/api/motion/moves', { spec, brief: a.brief || [a.hook, ...(a.lines || []), a.tagline].filter(Boolean).join(' / '), brand: { name } }); moves = p?.moves; planned = +p?.planCreditsUsed || 0; if (!Array.isArray(moves) || !moves.length) throw new Error('The moves for this film could not be written. Nothing else was charged. Try again, or pass moves yourself.'); }
|
|
22050
|
+
const r = await renderJob('motionmoving', { ...film, moves, ...mv }, 'MCP moving motion film');
|
|
22051
|
+
const raw = r?.raw || {};
|
|
22052
|
+
const notes = Array.isArray(raw.notes) && raw.notes.length ? `\nNOTE: ${raw.notes.join('; ')}` : '';
|
|
22053
|
+
return okVideo(`Motion film ready (${what}, ${raw.movingShots ?? moves.length} moving shot${(raw.movingShots ?? moves.length) === 1 ? '' : 's'}): ${r.url} [job ${r.jobId}]${planned ? ` Writing the moves was ${planned} credits.` : ''}${notes}\nIts spec is saved with the shots in it: make_motion_film({projectUrl: this url}) opens it, and changing the words costs only the render.`, r);
|
|
22054
|
+
}
|
|
22055
|
+
}
|
|
22035
22056
|
if (a.preview) {
|
|
22036
22057
|
const r = await renderJob('templatead', { ...film, config: { ...film.config, preview: true } }, 'MCP motion contact sheet');
|
|
22037
22058
|
if (r?.stillRendering) return ok(stillMsg(r), r);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.493",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "AI marketing agent and AI ad generator, run from your own AI agent. 1108 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",
|