hermoso 0.1.415 → 0.1.417

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.
Files changed (2) hide show
  1. package/mcp/tools.mjs +17 -13
  2. package/package.json +1 -1
package/mcp/tools.mjs CHANGED
@@ -4336,8 +4336,10 @@ function buildTools(rawServer, opts = {}, sink = null) {
4336
4336
  // MULTI-SHOT + its per-shot prompt cap (2026-10-05): the cap is a vendor rule the schema hides, so it is said here.
4337
4337
  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(', ');
4338
4338
  const _rm = d.remake; // the remake engines (recast_motion / restyle_video / edit_video engine), priced live
4339
- 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('; ')}` : ''}` : '';
4340
- 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}`;
4339
+ 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('; ')}${_rm.wanLabel ? ` (${_rm.wanLabel})` : ''}` : ''}${_rm.wanPrimeCredits ? `. wan-prime (Wan 3.0 Prime by name) ${Object.entries(_rm.wanPrimeCredits).map(([res, m]) => `${res} ${Object.entries(m).map(([s, c]) => `${s}s ${c}cr`).join(' / ')}`).join('; ')}` : ''}` : '';
4340
+ const _je = d.jobEngines; // the certified engine per job (watched side-by-side tests, 2026-10-19)
4341
+ const _jeLine = _je && _je.defaults ? `\nRecommended engine per job (watched side-by-side tests): ${Object.entries(_je.defaults).map(([k, v]) => `${k} → ${v}`).join(', ')}; a talking ad is one take of up to ${_je.talkingMaxSeconds}s. Pricier picks that looked no better for that job: ${Object.entries(_je.notNoticeablyBetter || {}).map(([k, v]) => `${k}: ${v.join(', ')}`).join('; ')}. If the user picks one, you may say once that the recommended one looks the same and costs less; never switch it.` : '';
4342
+ 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}${_jeLine}\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}`;
4341
4343
  return ok(text + connLine, d);
4342
4344
  }));
4343
4345
 
@@ -17794,7 +17796,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
17794
17796
  // so an import lands in the same list the app renders and syncs across devices.
17795
17797
  server.registerTool('import_from_cloud', {
17796
17798
  title: 'Import a Drive / OneDrive folder into the Library',
17797
- description: "Pull the files in a Google Drive or OneDrive FOLDER into this profile's Library, so they can be used like anything rendered here — published, scheduled, cloned, used as a product photo or a reference. Hermoso downloads each file with the user's own connected account (a Drive/OneDrive file is not public, so this is the only way in) and stores a durable Hermoso url for each. Give `folderId` from list_drive_files / list_onedrive_files with onlyFolders — omit it for the root. GOOGLE DRIVE ONLY SHOWS WHAT THE USER HANDED OVER: our Drive scope is `drive.file`, so Hermoso can see the files and folders it created plus the ones the user picked with the Google picker in the app, and NEVER their whole Drive — if a folder comes back empty, that is the answer, and the user picks it in the app once to make it reachable. OneDrive has no such limit. SUBFOLDERS ARE NOT WALKED and Google-native docs (Docs/Sheets/Slides) have no file to download: both are reported back BY NAME rather than silently dropped, along with anything too large or unreadable, so you can tell the user exactly what did and did not come across.",
17799
+ description: "Pull the files in a Google Drive or OneDrive FOLDER into this profile's Library, so they can be used like anything rendered here — published, scheduled, cloned, used as a product photo or a reference. Hermoso downloads each file with the user's own connected account (a Drive/OneDrive file is not public, so this is the only way in) and stores a durable Hermoso url for each. Give `folderId` from list_drive_files / list_onedrive_files with onlyFolders — omit it for the root. GOOGLE DRIVE ONLY SHOWS WHAT THE USER HANDED OVER: our Drive scope is `drive.file`, so Hermoso can see the files and folders it created plus the ones the user picked with the Google picker in the app, and NEVER their whole Drive. Picking a FOLDER hands over the folder alone, not the files in it, so if a folder comes back empty the reply carries a link that opens the picker inside it, where the user selects every file at once (Cmd+A / Ctrl+A); give them that link. OneDrive has no such limit. SUBFOLDERS ARE NOT WALKED and Google-native docs (Docs/Sheets/Slides) have no file to download: both are reported back BY NAME rather than silently dropped, along with anything too large or unreadable, so you can tell the user exactly what did and did not come across.",
17798
17800
  inputSchema: {
17799
17801
  provider: z.enum(['drive', 'onedrive']).describe('which cloud — `drive` is Google Drive, `onedrive` is Microsoft OneDrive'),
17800
17802
  folderId: z.string().optional().describe('the folder to import, from list_drive_files / list_onedrive_files (onlyFolders:true). Omit for the root of the drive.'),
@@ -17803,6 +17805,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
17803
17805
  outputSchema: {
17804
17806
  imported: z.array(z.object({ name: z.string().optional(), url: z.string().optional(), kind: z.string().optional(), bytes: z.number().optional() })).optional(),
17805
17807
  skipped: z.array(z.object({ name: z.string().optional(), why: z.string().optional() })).optional(),
17808
+ note: z.string().optional(),
17806
17809
  },
17807
17810
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
17808
17811
  }, wrap(async (a) => {
@@ -17819,7 +17822,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
17819
17822
  .map((x) => ({ url: x.url, kind: x.kind || '', model: a.provider === 'drive' ? 'Imported from Google Drive' : 'Imported from OneDrive', name: x.name || '', at: Date.now() }));
17820
17823
  if (rows.length) await writeStore('heist.assets.v1', [...rows, ...list].slice(0, 300));
17821
17824
  }
17822
- if (!imported.length && !skipped.length) return ok('That folder is empty — nothing to import.', { imported: [], skipped: [] });
17825
+ if (!imported.length && !skipped.length) return ok(a.provider === 'drive' && d.note ? d.note : 'That folder is empty — nothing to import.', { imported: [], skipped: [], ...(d.note ? { note: d.note } : {}) }); // an empty DRIVE folder is usually the drive.file scope: the server's note says why and links the fix
17823
17826
  const lines = [
17824
17827
  imported.length ? `Imported ${imported.length} file${imported.length === 1 ? '' : 's'} into the Library:` : 'Nothing was imported.',
17825
17828
  ...imported.map((x) => ` • ${x.name || x.url} → ${x.url}`),
@@ -17830,7 +17833,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
17830
17833
 
17831
17834
  server.registerTool('list_drive_files', {
17832
17835
  title: 'List Google Drive files',
17833
- description: 'List the Google Drive files & folders Hermoso can reach — the ones it created, plus any the user handed over with the Google file picker in the app (the drive.file scope exposes nothing else, never their entire Drive). This is how you find the id of a file the user picked. Filter by query (name contains …), folderId (contents of a folder), or onlyFolders:true. Paginate with pageToken. Read-only.',
17836
+ description: 'List the Google Drive files & folders Hermoso can reach — the ones it created, plus any the user handed over with the Google file picker in the app (the drive.file scope exposes nothing else, never their entire Drive). This is how you find the id of a file the user picked. Filter by query (name contains …), folderId (contents of a folder), or onlyFolders:true. Paginate with pageToken. Read-only. A FOLDER: picking a folder hands over the folder alone, not its files, so a folder can list empty or partly; the reply then carries `pickUrl`, an app link that opens the Google Drive picker inside that folder where the user selects every file at once (Cmd+A / Ctrl+A). Give the user that link, then list again.',
17834
17837
  inputSchema: {
17835
17838
  query: z.string().optional().describe('only files whose name contains this'),
17836
17839
  folderId: z.string().optional().describe('list the contents of this folder id'),
@@ -17839,12 +17842,13 @@ function buildTools(rawServer, opts = {}, sink = null) {
17839
17842
  pageToken: z.string().optional().describe('cursor from a previous call'),
17840
17843
  includeTrashed: z.boolean().optional().describe('include trashed files (default false)'),
17841
17844
  },
17842
- outputSchema: { files: z.array(z.any()).optional(), cursor: z.string().nullable().optional() },
17845
+ outputSchema: { files: z.array(z.any()).optional(), cursor: z.string().nullable().optional(), note: z.string().optional(), pickUrl: z.string().optional(), folderVisible: z.boolean().nullable().optional() },
17843
17846
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
17844
17847
  }, wrap(async (a) => {
17845
17848
  const d = await apiGet('/api/drive/files', a);
17846
17849
  const lines = (d.files || []).map(f => `• ${f.name} (${f.id})${f.mimeType && f.mimeType.includes('folder') ? ' [folder]' : ''}${f.webViewLink ? ` — ${f.webViewLink}` : ''}`);
17847
- return ok(`${(d.files || []).length} item(s):\n${lines.join('\n') || '(none)'}`, d);
17850
+ // A folder listing carries the server's note (why it is empty or partial, and the link that fixes it): print it, never a bare "(none)".
17851
+ return ok(`${(d.files || []).length} item(s):\n${lines.join('\n') || '(none)'}${d.note ? `\n${d.note}${d.pickUrl && !String(d.note).includes(d.pickUrl) ? ` Link: ${d.pickUrl}` : ''}` : ''}`, d);
17848
17852
  }));
17849
17853
  server.registerTool('get_drive_file', {
17850
17854
  title: 'Get a Drive file’s details',
@@ -18650,7 +18654,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
18650
18654
  creative: z.object({}).passthrough().describe('the FULL structured output of plan_ad (must contain video_storyboard)'),
18651
18655
  creator: z.string().optional().describe('CAST A SAVED CREATOR in this ad — their id from list_creators, or the name you know them by (“Sarah”), or a PRESET AI creator from list_creators presets by exact name or id (free, no generation). Their saved portrait becomes the on-camera identity for the whole spot, so the same face carries across every act and across every ad you render for this brand — and because we already have their picture, the character portrait this pipeline would otherwise generate is skipped, so casting somebody costs LESS than not casting them. Omit to let the ad cast a fresh person — EXCEPT for a CREATOR account (onboarded from their own @handle): their own saved likeness is cast by default when the plan has a person on camera and the account is on a paid Hermoso plan (a free account gets a fresh AI person), and the read-back says `default:true`; pass "none" to render without them. Refused for free, with nothing rendered, if the name matches nobody or more than one creator, if an explicitly named creator is cast on a plan with nobody on camera, or if a REAL person is cast on a free plan. ' + LIKENESS_TERMS),
18652
18656
  faceRoute: z.enum(['face_lane']).optional().describe(FACE_ROUTE_DESC),
18653
- model: z.string().optional().describe('video model id from hermoso_capabilities (default: the plan’s pick). Naming one is a DELIBERATE pick — the server asks before ever swapping it (no silent fallback)'),
18657
+ model: z.string().optional().describe('video model id from hermoso_capabilities (default: the recommended engine for the plan: wan-3 for a person talking to camera in one take up to 15s, seedance-2.5 otherwise). Naming one is a DELIBERATE pick — the server asks before ever swapping it (no silent fallback); a pricier pick the side-by-side tests found no better comes back with a one-line GUIDANCE note you may pass on once'),
18654
18658
  durationSeconds: z.number().optional().describe('total ad length in seconds (supported range 4–180; outside that it is clamped). Omit to honor the plan’s own duration — that is almost always right. This only RE-TIMES an already-authored board (its scenes are scaled to fit), it does NOT re-write it, so to change the length of the ad the user asked for, re-run plan_ad with durationSeconds instead. A length that fits ONE clip of the render model renders as one continuous pass; longer is stitched from acts filled to that model’s clip maximum with the remainder last — the maximum is 15s on most models and 30s on the longest-clip one, so use dryRun:true to see the exact act split for free before spending.'),
18655
18659
  aspectRatio: z.string().optional().describe('output aspect ratio, e.g. 9:16 (default) / 1:1 / 16:9'),
18656
18660
  resolution: z.enum(['480p', '720p', '1080p', '4k']).optional().describe("'1080p' default (what we ship and bill for); '480p'/'720p' = cheaper draft passes, '4k' = premium final delivery (more credits). NOT EVERY MODEL OFFERS EVERY TIER — this enum is what the tool accepts, and each model's OWN `resolutions` list in hermoso_capabilities is what it can actually render (the longest-clip 30s model, for one, tops out at 720p). Ask for a tier the chosen model does not list and it is rendered at that model's best available tier instead, with nothing in the reply saying so — so check `resolutions` before promising anyone 1080p or 4k."),
@@ -19139,7 +19143,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
19139
19143
  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.'),
19140
19144
  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).'),
19141
19145
  aspectRatio: z.string().optional().describe("default '9:16'"),
19142
- model: z.string().optional().describe('video model id from hermoso_capabilities; a named model is never swapped without asking. Omit to let the router pick'),
19146
+ model: z.string().optional().describe('video model id from hermoso_capabilities; a named model is never swapped without asking. Omit to let the router pick. RECOMMENDED per job (jobEngines in hermoso_capabilities, from watched side-by-side tests): wan-3 for a person talking to camera (one take up to 15s), seedance-2.5 for product, lifestyle and 30s single takes. If the user picks a pricier model the tests found no better for that job (e.g. wan-3-prime or seedance-2.5 for a talking clip), you may say once that the recommended one looks the same and costs less; never switch it'),
19143
19147
  faceRoute: z.enum(['face_lane']).optional().describe(FACE_ROUTE_DESC),
19144
19148
  resolution: z.enum(['480p', '720p', '1080p', '4k']).optional().describe("'1080p' default (what we ship and bill for); '480p'/'720p' = cheaper draft passes, '4k' = premium final delivery (more credits). NOT EVERY MODEL OFFERS EVERY TIER — this enum is what the tool accepts, and each model's OWN `resolutions` list in hermoso_capabilities is what it can actually render. Ask for a tier the chosen model does not list and it is rendered at that model's best available tier instead, with nothing in the reply saying so — so check `resolutions` before promising anyone 1080p or 4k."),
19145
19149
  cameraMove: z.enum(['orbit', 'orbit_left', 'orbit_half', 'orbit_full', 'rise', 'crane_up', 'push_in', 'pull_back', 'reveal']).optional().describe('A named camera move around the still in refImage, spelled exactly as the enum gives it: an orbit (a quarter turn, the default), orbiting left, a half turn or a full turntable, a rise, a crane up, a push in, a pull back, or a reveal. Only the camera-controls model renders one (minimax-h3-max-camera, listed with its moves in hermoso_capabilities): pass it with model omitted and that model is picked, or with that model named; any other named model is refused by name with nothing charged. Needs refImage.'),
@@ -21428,14 +21432,14 @@ function memoryNoteVerdict(text) {
21428
21432
  // described look) runs as a video edit: same engines, same price, same free preservation read-back as edit_video.
21429
21433
  server.registerTool('restyle_video', {
21430
21434
  title: 'Restyle a video',
21431
- description: "RESTYLE a clip into a new LOOK: every frame redrawn, shots, motion, camera, framing, timing and sound kept. Looks: `Restyle looks` in hermoso_capabilities (claymation, knitted yarn, cel-shaded CG anime…), your own words, or a look BUILT FROM YOUR IMAGES (styleImages); characters draws your own characters in. engine auto (default) picks by what is in the clip: a real-looking person -> kling (Kling O3 Edit, 3-15s); none -> wan (Wan 3.0 Prime, ≤15s) else seedance (Seedance 2.5, 4-30s). Paid; dryRun quotes it. One change only: edit_video.",
21435
+ description: "RESTYLE a clip into a new LOOK: every frame redrawn, shots, motion, camera, framing, timing and sound kept. Looks: `Restyle looks` in hermoso_capabilities (claymation, knitted yarn, cel-shaded CG anime…), your own words, or a look BUILT FROM YOUR IMAGES (styleImages); characters draws your own characters in. engine auto (default) picks by what is in the clip: a real-looking person -> kling (Kling O3 Edit, 3-15s); none -> wan (Wan 3.0, recommended, ≤15s) else seedance (Seedance 2.5, 4-30s); wan-prime = Wan 3.0 Prime by name (looked the same side by side, costs more: say so once if the user picks it). Paid; dryRun quotes it. One change only: edit_video.",
21432
21436
  inputSchema: {
21433
21437
  video: z.string().describe('the source video URL (a render, job result or list_library)'),
21434
21438
  style: z.string().optional().describe('a look id from hermoso_capabilities (e.g. claymation, knitted-yarn, cel-shaded-anime-cg)'),
21435
21439
  describe: z.string().optional().describe('a look in the user’s own words, when no preset fits, or extra detail on top of a preset'),
21436
21440
  styleImages: z.array(z.string()).max(9).optional().describe('pictures that ARE the look (their style, never their subject)'),
21437
21441
  characters: z.array(z.string()).max(4).optional().describe('characters to draw the people as. ' + LIKENESS_TERMS),
21438
- engine: z.enum(['auto', 'seedance', 'wan', 'kling']).optional().describe('default auto'),
21442
+ engine: z.enum(['auto', 'seedance', 'wan', 'wan-prime', 'kling']).optional().describe('default auto'),
21439
21443
  resolution: z.enum(['480p', '720p', '1080p']).optional().describe('Seedance and Wan; default 1080p'),
21440
21444
  faceRoute: z.enum(['face_lane']).optional().describe(FACE_ROUTE_CLIP_DESC),
21441
21445
  },
@@ -21605,13 +21609,13 @@ function memoryNoteVerdict(text) {
21605
21609
 
21606
21610
  server.registerTool('recast_motion', {
21607
21611
  title: 'Recast motion',
21608
- 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.",
21612
+ 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 (looked the same side by side, costs more: say so once if the user picks it). 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.",
21609
21613
  inputSchema: {
21610
21614
  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),
21611
21615
  images: z.array(z.string()).max(8).optional().describe('seedance/wan: more pictures, @Image2…: a character, product, outfit or place'),
21612
21616
  video: z.string().describe('the reference video whose motion to re-perform'),
21613
21617
  prompt: z.string().optional().describe("optional, e.g. 'same moves, new location: Tokyo at night'"),
21614
- engine: z.enum(['auto', 'h3', 'seedance', 'wan', 'kling']).optional().describe('default auto'),
21618
+ engine: z.enum(['auto', 'h3', 'seedance', 'wan', 'wan-prime', 'kling']).optional().describe('default auto'),
21615
21619
  resolution: z.enum(['480p', '720p', '1080p']).optional().describe('seedance and wan default 1080p; h3 takes 720p or 1080p, default the clip’s own'),
21616
21620
  orientation: z.enum(['video', 'image']).optional().describe("kling only: which aspect to keep: the video's (default) or the image's"),
21617
21621
  faceRoute: z.enum(['face_lane']).optional().describe(FACE_ROUTE_CLIP_DESC),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.415",
3
+ "version": "0.1.417",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
5
  "description": "AI marketing agent and AI ad generator, run from your own AI agent. 912 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, 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",