hermoso 0.1.380 → 0.1.381

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.
@@ -1256,7 +1256,7 @@ export const neutralNote = (t) => {
1256
1256
  // sweeps and drift watches queued for later (measured on a fresh local account: one `agentidle` "nudge", queued for
1257
1257
  // tomorrow, would have shown as "1 render in progress"). Mirrors server.js JOB_TYPES_HIDDEN_FROM_V1, and the check
1258
1258
  // fails if that list gains a type this one lacks. A job queued for LATER is not in progress either.
1259
- export const PANEL_HIDDEN_JOB_TYPES = Object.freeze(['purge', 'agentidle', 'agentsetupmail', 'uploadretention', 'connectordrift', 'falwatch', 'faceassetsweep', 'searchvis', 'mcpcanary', 'welcomeads', 'lifecycleedu', 'connhealth']);
1259
+ export const PANEL_HIDDEN_JOB_TYPES = Object.freeze(['purge', 'agentidle', 'agentsetupmail', 'uploadretention', 'connectordrift', 'falwatch', 'faceassetsweep', 'searchvis', 'mcpcanary', 'welcomeads', 'lifecycleedu', 'connhealth', 'chargednothing', 'emailfeedback']);
1260
1260
  const ageText = (h) => (h == null || !Number.isFinite(+h) ? '' : +h < 1 ? 'just now' : +h < 48 ? `${Math.round(+h)}h ago` : `${Math.round(+h / 24)}d ago`);
1261
1261
  const VIDEO_RE = /\.(mp4|webm|mov|m4v)([?#]|$)/i;
1262
1262
  const clip = (s, n) => { const t = String(s ?? '').replace(/\s+/g, ' ').trim(); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
package/mcp/tools.mjs CHANGED
@@ -4032,9 +4032,11 @@ function buildTools(rawServer, opts = {}, sink = null) {
4032
4032
  // and the one-tap looks restyle_video takes. Derived, never a hand list.
4033
4033
  const _extLine = (d.options?.video?.models || []).filter(m => m.extend).map(m => `${m.id} +${m.extend.minSeconds}${m.extend.maxSeconds !== m.extend.minSeconds ? `-${m.extend.maxSeconds}` : ''}s`).join(', ');
4034
4034
  const _rsLine = (d.restyleStyles || []).map(x => x.id).join(', ');
4035
+ // MULTI-SHOT + its per-shot prompt cap (2026-10-05): the cap is a vendor rule the schema hides, so it is said here.
4036
+ const _shotLine = (d.options?.video?.models || []).filter(m => m.multiShot).map(m => `${m.id}${m.shotPromptMaxChars ? ` (each shot prompt at most ${m.shotPromptMaxChars} characters; longer is refused, never cut)` : ''}`).join(', ');
4035
4037
  const _rm = d.remake; // the remake engines (recast_motion / restyle_video / edit_video engine), priced live
4036
4038
  const _rmLine = _rm ? `\nRemake engines (recast_motion, restyle_video, edit_video engine): auto picks, for a clip with no real-looking person, ${Object.entries(_rm.defaults || {}).map(([k, v]) => `${k} → ${v}`).join(', ')}; with a person in the clip, ${Object.entries(_rm.faceDefaults || {}).map(([k, v]) => `${k} → ${v}`).join(', ')}${_rm.personClipLibraryDefaults ? ` (a person clip our face library can carry, i.e. one of our renders or faceRoute on a paid plan: ${Object.entries(_rm.personClipLibraryDefaults).map(([k, v]) => `${k} → ${v}`).join(', ')})` : ''}. seedance ${(_rm.seconds || []).join('-')}s, up to ${_rm.imagesMax} pictures, ${Object.entries(_rm.seedanceCredits || {}).map(([res, m]) => `${res} ${Object.entries(m).map(([s, c]) => `${s}s ${c}cr`).join(' / ')}`).join('; ')}${_rm.wanCredits ? `. wan clips up to ${_rm.wanMaxSourceSeconds}s, ${Object.entries(_rm.wanCredits).map(([res, m]) => `${res} ${Object.entries(m).map(([s, c]) => `${s}s ${c}cr`).join(' / ')}`).join('; ')}` : ''}` : '';
4037
- const text = `Image: ${d.image ? img : 'unavailable'}\nVideo: ${d.video ? vid : 'unavailable'}\n${_lenLine}${_extLine ? `\nExtend a clip (generate_video extend:true + refVideo): ${_extLine}` : ''}${_rsLine ? `\nRestyle looks (restyle_video style): ${_rsLine}` : ''}${_rmLine}\n${_pickLine}\n${_resLine}${_motionLine}${_avLine}\nVoice engines (generate_voice): ${voice}\nWriting models (generate_text): ${llm}\ncanEdit:${d.canEdit} canAvatar:${d.canAvatar}\nRecipes (${(d.recipes || []).length}): ${(d.recipes || []).slice(0, 20).map(r => r.id).join(', ')}…\n\n${CAPABILITY_MAP}`;
4039
+ const text = `Image: ${d.image ? img : 'unavailable'}\nVideo: ${d.video ? vid : 'unavailable'}\n${_lenLine}${_extLine ? `\nExtend a clip (generate_video extend:true + refVideo): ${_extLine}` : ''}${_shotLine ? `\nMulti-shot clips (generate_video shots): ${_shotLine}` : ''}${_rsLine ? `\nRestyle looks (restyle_video style): ${_rsLine}` : ''}${_rmLine}\n${_pickLine}\n${_resLine}${_motionLine}${_avLine}\nVoice engines (generate_voice): ${voice}\nWriting models (generate_text): ${llm}\ncanEdit:${d.canEdit} canAvatar:${d.canAvatar}\nRecipes (${(d.recipes || []).length}): ${(d.recipes || []).slice(0, 20).map(r => r.id).join(', ')}…\n\n${CAPABILITY_MAP}`;
4038
4040
  return ok(text + connLine, d);
4039
4041
  }));
4040
4042
 
@@ -18671,7 +18673,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
18671
18673
  title: 'Generate video',
18672
18674
  description: 'Render a RAW video clip from your own prompt and return its served mp4 URL. For finished brand ADS prefer render_ad (it runs the Studio quality pipeline — composited text, clean speech, end card, music); use this for raw/experimental clips or precise manual control. ONE generation = one continuous clip up to the model’s longest listed duration — the longest-clip model in the catalog today renders a full multi-beat spot of up to 30 SECONDS in ONE unbroken take with native synchronized audio, so never assume a generic 8–10s cap and never stitch something that fits one clip; durationSeconds must be one of the model’s durations from hermoso_capabilities, which is the live list. TO GET A SPECIFIC MODEL, NAME IT in `model`: an unnamed render is routed by the server’s own auto-pool, which is narrower than the catalog, so the longest-clip and highest-resolution models are reached by naming them. Renders take 1–3 min. refImage anchors the opening frame; ttsScript adds a voiceover. AUDIO IS NOT FREE AND NOT OPTIONAL BY DEFAULT: a clip delivered with no audio of its own gets a music bed composed and CHARGED on top of the render (see musicMood and audio) — on a cheap short draft the bed can cost as much as the clip. Pass refVideo (a clip URL) to EDIT an existing video instead — the omni engine transforms that clip per your prompt, inheriting its canvas + length (aspectRatio/durationSeconds are ignored for an edit). RAW MODEL ACCESS: by default a few small guards are appended (packaging/label safety with no reference image, a negative prompt where the model takes one, reference-binding lines) and hex colour codes become colour names; ' + RAW_TOOL_NOTE + ' Spends credits (Starter plan is video-blocked server-side).',
18673
18675
  inputSchema: {
18674
- prompt: z.string().describe('the video prompt / shot description (for a refVideo edit, this is the transformation instruction)'),
18676
+ prompt: z.string().optional().describe('the video prompt / shot description (for a refVideo edit, this is the transformation instruction); optional with `shots`'),
18675
18677
  raw: z.boolean().optional().describe('RAW MODEL ACCESS: dispatch this prompt to the model BYTE-IDENTICAL — no appended packaging/label guidance, no negative prompt, no reference-binding lines, no hex-to-colour-name rewrite. Use it when you want the model itself rather than Hermoso\'s render craft. Two vendor-required fixes still apply: extra @ImageN tokens are dropped and an over-long prompt is trimmed at a sentence. Billing, durable delivery and per-model validation are unchanged.'),
18676
18678
  refImage: z.string().optional().describe('local path or URL to anchor the first frame. ' + LIKENESS_TERMS),
18677
18679
  speak: z.string().optional().describe('A PERSON SAYING THESE EXACT WORDS TO CAMERA — the default for any "make my photo talk" / spokesperson / talk-to-camera ask. Pass with `creator` or a portrait as `refImage`: the video model films them saying it in their own voice, matched to who they are, and the length follows the words (leave durationSeconds out). `prompt` is then the staging (e.g. "natural", "walking in a park"). Real footage, not an animated photo; generate_avatar is the animated-photo look, only when asked for by name'),
@@ -18681,7 +18683,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
18681
18683
  refVideo: z.string().optional().describe("URL of an existing video to EDIT rather than generate from scratch — the clip is transformed per your prompt, inheriting the SOURCE clip’s canvas (aspect ratio) and, on every model hermoso_capabilities marks `sourceLength`, its LENGTH too: those endpoints have no duration parameter, their listed `durations` are the per-second price ladder, and a durationSeconds you send is reported back as unused rather than silently dropped. Trim the source to change the length. Omit `model` for the default editor, or name a model whose videoEdit is true in hermoso_capabilities (a named model that cannot edit is refused, nothing charged); refImage rides along as the look of what the edit adds. With extend:true this is instead the clip to EXTEND. Omit to generate a fresh clip."),
18682
18684
  endImage: z.string().optional().describe('local path or URL of the LAST frame: the clip travels from refImage (required with it) to this image. Only models with endFrame true in hermoso_capabilities take it; any other named model is refused by name, nothing charged.'),
18683
18685
  loop: z.boolean().optional().describe('true = a seamless loop whose last frame flows back into its first. Only models with loop true in hermoso_capabilities; needs refImage and cannot be combined with endImage.'),
18684
- shots: z.array(z.object({ prompt: z.string().describe('what happens in this shot'), seconds: z.number().int().min(1).max(15).describe('this shot’s length in whole seconds') })).optional().describe('MULTI-SHOT: one clip cut into these shots, in order. The seconds must add up to a length the model renders; replaces `prompt` (send either). Only models with multiShot true in hermoso_capabilities.'),
18686
+ shots: z.array(z.object({ prompt: z.string().describe('what happens in this shot'), seconds: z.number().int().min(1).max(15).describe('this shot’s length in whole seconds') })).optional().describe('MULTI-SHOT: one clip cut into these shots, in order. The seconds must add up to a length the model renders; replaces `prompt` (send either). Only models with multiShot true in hermoso_capabilities. Each shot prompt is capped per model (shotPromptMaxChars there; Kling 3.0: 512 characters); a longer one is refused free, never cut.'),
18685
18687
  extend: z.boolean().optional().describe('true = EXTEND refVideo: the same clip continues per your prompt for durationSeconds more (each model’s extend.minSeconds..maxSeconds in hermoso_capabilities), delivered as ONE clip, source then continuation. Needs model named (a model with extend in hermoso_capabilities).'),
18686
18688
  interactionId: z.string().optional().describe('with extend:true on an Omni model: the interactionId returned by an earlier render on that model — continues it from its own stored context instead of re-uploading refVideo.'),
18687
18689
  durationSeconds: z.number().optional().describe('length of THIS ONE clip in seconds — pick one of the CHOSEN model’s listed durations from hermoso_capabilities (never a generic guess: the lists differ per model, from 4–8s on the short models up to 30s on the longest-clip one). This is a single continuous generation, so it CANNOT exceed that model’s longest clip: a longer ask is REFUSED with nothing rendered and nothing charged (it is never quietly truncated). Past ~15s only the long-clip models qualify, and an unnamed render is routed by the narrower auto-pool — so NAME the model in `model` when you are asking for a long single take. For a spot longer than any one clip, use plan_ad with durationSeconds then render_ad, which stitches acts of at most one model clip each (on a 15s-clip model, 40s = 15+15+10).'),
@@ -18702,7 +18704,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
18702
18704
  audio: z.boolean().optional().describe('default true. false = render SILENT: no native model audio, no music bed, and no bed charge held or billed. This is the ONLY way to decline the automatic bed (see musicMood) — leave it alone for anything that should have sound, and do not combine it with ttsScript.'),
18703
18705
  },
18704
18706
  outputSchema: { ...JOB_OUT,
18705
- refused: z.string().optional().describe("set when NOTHING was rendered and nothing charged — 'duration_exceeds_single_clip' or 'aspect_unsupported'"),
18707
+ refused: z.string().optional().describe("set when NOTHING was rendered and nothing charged — 'duration_exceeds_single_clip', 'aspect_unsupported' or 'missing_prompt'"),
18706
18708
  maxSingleClipSeconds: z.number().optional().describe('the longest single clip any connected video model can render'),
18707
18709
  askedSeconds: z.number().optional().describe('the durationSeconds that could not be honored'),
18708
18710
  askedAspectRatio: z.string().optional().describe('the aspectRatio that could not be honored'),
@@ -18712,6 +18714,10 @@ function buildTools(rawServer, opts = {}, sink = null) {
18712
18714
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
18713
18715
  _meta: openaiMeta(AD_RESULT_URI, 'Rendering your video…', 'Video ready'),
18714
18716
  }, wrap(async (a) => {
18717
+ // A PROMPT OR SHOTS, EITHER ONE (2026-10-05). `shots` replaces `prompt` (Kling sends multi_prompt and no prompt at all),
18718
+ // so the schema leaves prompt optional and the one refusal lives here: neither is nothing to render. Free, no network.
18719
+ const _hasShots = Array.isArray(a.shots) && a.shots.length > 0;
18720
+ if (!String(a.prompt || '').trim() && !_hasShots) return ok('NOTHING WAS RENDERED and nothing was charged. Say what the clip should show: send `prompt`, or `shots` (a list of {prompt, seconds}) for a multi-shot clip on a model with multiShot true in hermoso_capabilities.', { refused: 'missing_prompt' });
18715
18721
  // NEVER SILENTLY TRUNCATE A LENGTH ASK. One generation = ONE continuous clip, and workVideo resolves an
18716
18722
  // over-long ask by picking the longest clip the model has — so a 40s generate_video call used to render 15s and
18717
18723
  // report success. A refusal that names the real ceiling and the real alternative is the honest answer, and it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.380",
3
+ "version": "0.1.381",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
5
  "description": "Marketing on autopilot, run from your own AI agent. 897 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",