hermoso 0.1.452 → 0.1.454
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/arg-guard.mjs +8 -0
- package/mcp/tools.mjs +34 -16
- package/package.json +1 -1
package/mcp/arg-guard.mjs
CHANGED
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
// Byte-identical in mcp/ and cli/mcp/ (the CLI ships rooted at cli/), and imported by server.js for /v1 and the Studio.
|
|
17
17
|
|
|
18
18
|
export const UNKNOWN_ARGS_SYM = Symbol.for('hermoso.unknownArgs');
|
|
19
|
+
// The same facts as data (2026-10-27, customer ask: the note was only in the text). Rides the parsed args beside the
|
|
20
|
+
// sentence; a refused call carries it under REFUSED so the handler wrapper answers without running the tool.
|
|
21
|
+
export const UNKNOWN_ARGS_DETAIL_SYM = Symbol.for('hermoso.unknownArgsDetail');
|
|
22
|
+
export const UNKNOWN_ARGS_REFUSED_SYM = Symbol.for('hermoso.unknownArgsRefused');
|
|
23
|
+
/** [{name, didYouMean}] — didYouMean is a key THIS tool declares, or null. The structured twin of unknownArgsNote. */
|
|
24
|
+
export function unknownArgsDetail(unknown = [], declared = []) {
|
|
25
|
+
return unknown.map((k) => ({ name: k, didYouMean: didYouMean(k, declared) || null }));
|
|
26
|
+
}
|
|
19
27
|
|
|
20
28
|
/**
|
|
21
29
|
* 'typo' (default) | 'reject' | 'warn' | 'off' from the environment value. Anything unrecognised is `typo`:
|
package/mcp/tools.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
12
12
|
// The UTF-16 well-formedness law — see wellFormedServer() below for why it is applied here, and
|
|
13
13
|
// ./well-formed.mjs for why that specifier is the only one that can work in a byte-identical twin.
|
|
14
14
|
import { wellFormedValue, wellFormedString } from './well-formed.mjs';
|
|
15
|
-
import { UNKNOWN_ARGS_SYM, argGuardMode, argGuardRefuses, declaredArgKeys, didYouMean, unknownArgs, unknownArgsNote } from './arg-guard.mjs'; // an undeclared argument is named back, never silently stripped (2026-10-26)
|
|
15
|
+
import { UNKNOWN_ARGS_SYM, UNKNOWN_ARGS_DETAIL_SYM, UNKNOWN_ARGS_REFUSED_SYM, argGuardMode, argGuardRefuses, declaredArgKeys, didYouMean, unknownArgs, unknownArgsNote, unknownArgsDetail } from './arg-guard.mjs'; // an undeclared argument is named back, never silently stripped (2026-10-26)
|
|
16
16
|
// WHICH CONNECTOR A TOOL NEEDS — the same table and the same decision the Studio chat applies (lib/studio-roster.mjs
|
|
17
17
|
// re-exports every symbol from here). `./roster-scope.mjs` is the only specifier that resolves in a byte-identical
|
|
18
18
|
// twin, for the same reason ./well-formed.mjs is. See applyToolGates() for the seam and roster-scope.mjs for the law.
|
|
@@ -2811,6 +2811,7 @@ export const undeclaredArgs = (h, input) => {
|
|
|
2811
2811
|
if (catType && !/never/i.test(catType)) return [];
|
|
2812
2812
|
return keys.filter((k) => !Object.prototype.hasOwnProperty.call(shape, k));
|
|
2813
2813
|
};
|
|
2814
|
+
export const undeclaredArgsStructured = (extra, h) => ({ error: 'unknown_arguments', unknownArguments: unknownArgsDetail(extra, h && h.inputSchema && typeof h.inputSchema.shape === 'object' ? Object.keys(h.inputSchema.shape) : []) });
|
|
2814
2815
|
export const undeclaredArgsText = (name, extra, h) => {
|
|
2815
2816
|
const shape = h && h.inputSchema && h.inputSchema.shape;
|
|
2816
2817
|
const takes = shape && typeof shape === 'object' ? Object.keys(shape) : [];
|
|
@@ -2830,7 +2831,7 @@ export async function legacyToolAnswer(name, request, extra, ctx) {
|
|
|
2830
2831
|
const fn = h && (h.handler || h.callback);
|
|
2831
2832
|
if (typeof fn !== 'function') return withStaleNote({ content: [{ type: 'text', text: `${name} was renamed to ${spec.to}, which is not part of this session's roster.` }], isError: true }, ctx);
|
|
2832
2833
|
let input = request?.params?.arguments && typeof request.params.arguments === 'object' ? request.params.arguments : {};
|
|
2833
|
-
{ const extraKeys = undeclaredArgs(h, input); if (extraKeys.length) return withStaleNote({ content: [{ type: 'text', text: undeclaredArgsText(spec.to, extraKeys, h) }], isError: true }, ctx); }
|
|
2834
|
+
{ const extraKeys = undeclaredArgs(h, input); if (extraKeys.length) return withStaleNote({ content: [{ type: 'text', text: undeclaredArgsText(spec.to, extraKeys, h) }], structuredContent: undeclaredArgsStructured(extraKeys, h), isError: true }, ctx); }
|
|
2834
2835
|
if (h.inputSchema && typeof h.inputSchema.safeParse === 'function') {
|
|
2835
2836
|
const parsed = h.inputSchema.safeParse(input);
|
|
2836
2837
|
if (!parsed.success) {
|
|
@@ -3240,7 +3241,7 @@ export function installHeldToolCalls(mcp, ctx) {
|
|
|
3240
3241
|
const fn = h.handler || h.callback;
|
|
3241
3242
|
if (typeof fn === 'function') {
|
|
3242
3243
|
let input = request?.params?.arguments && typeof request.params.arguments === 'object' ? request.params.arguments : {};
|
|
3243
|
-
{ const extraKeys = undeclaredArgs(h, input); if (extraKeys.length) return { content: [{ type: 'text', text: undeclaredArgsText(name, extraKeys, h) }], isError: true }; }
|
|
3244
|
+
{ const extraKeys = undeclaredArgs(h, input); if (extraKeys.length) return { content: [{ type: 'text', text: undeclaredArgsText(name, extraKeys, h) }], structuredContent: undeclaredArgsStructured(extraKeys, h), isError: true }; }
|
|
3244
3245
|
if (h.inputSchema && typeof h.inputSchema.safeParse === 'function') {
|
|
3245
3246
|
const parsed = h.inputSchema.safeParse(input);
|
|
3246
3247
|
if (!parsed.success) {
|
|
@@ -3598,19 +3599,35 @@ export function guardUnknownArgs(server) {
|
|
|
3598
3599
|
const mode = argGuardMode();
|
|
3599
3600
|
if (unknown.length && mode !== 'off') {
|
|
3600
3601
|
try { console.error(`[mcp-unknown-arg] tool=${toolName} keys=${unknown.join(',')} mode=${mode}`); } catch {}
|
|
3601
|
-
|
|
3602
|
+
// REFUSED WITHOUT RUNNING, answered by withUnknownArgsNote so the refusal carries `unknownArguments` as data
|
|
3603
|
+
// (a throw here would reach the caller as text only). The SDK's own validation still runs first, so a call
|
|
3604
|
+
// that is malformed in another way is refused for that too.
|
|
3605
|
+
if (argGuardRefuses(mode, unknown, declared)) {
|
|
3606
|
+
await orig.call(this, tool, Object.fromEntries(Object.entries(args || {}).filter(([k]) => !unknown.includes(k))), toolName);
|
|
3607
|
+
const marker = {};
|
|
3608
|
+
Object.defineProperty(marker, UNKNOWN_ARGS_REFUSED_SYM, { value: { text: unknownArgsNote(toolName, unknown, declared, { rejected: true }), detail: unknownArgsDetail(unknown, declared) }, enumerable: false });
|
|
3609
|
+
return marker;
|
|
3610
|
+
}
|
|
3602
3611
|
}
|
|
3603
3612
|
const out = await orig.call(this, tool, args, toolName);
|
|
3604
|
-
if (unknown.length && mode !== 'off' && out && typeof out === 'object') { try { Object.defineProperty(out, UNKNOWN_ARGS_SYM, { value: unknownArgsNote(toolName, unknown, declared), enumerable: false, configurable: true }); } catch {} }
|
|
3613
|
+
if (unknown.length && mode !== 'off' && out && typeof out === 'object') { try { Object.defineProperty(out, UNKNOWN_ARGS_SYM, { value: unknownArgsNote(toolName, unknown, declared), enumerable: false, configurable: true }); Object.defineProperty(out, UNKNOWN_ARGS_DETAIL_SYM, { value: unknownArgsDetail(unknown, declared), enumerable: false, configurable: true }); } catch {} }
|
|
3605
3614
|
return out;
|
|
3606
3615
|
};
|
|
3607
3616
|
try { Object.defineProperty(server, 'validateToolInput', { value: guarded, configurable: true, writable: true }); Object.defineProperty(server, UNKNOWN_ARGS_SYM, { value: true }); } catch {}
|
|
3608
3617
|
return server;
|
|
3609
3618
|
}
|
|
3619
|
+
// THE NOTE IS TEXT AND DATA (2026-10-27): `ignoredArguments` [{name, didYouMean}] rides structuredContent on a call
|
|
3620
|
+
// that ran, and `unknownArguments` on one that was refused, so a program reads it without parsing the sentence.
|
|
3610
3621
|
export function withUnknownArgsNote(result, args) {
|
|
3611
3622
|
const note = args && typeof args === 'object' ? args[UNKNOWN_ARGS_SYM] : '';
|
|
3612
3623
|
if (!note || !result || typeof result !== 'object' || !Array.isArray(result.content)) return result;
|
|
3613
|
-
|
|
3624
|
+
const detail = args[UNKNOWN_ARGS_DETAIL_SYM] || [];
|
|
3625
|
+
const sc = result.structuredContent && typeof result.structuredContent === 'object' && !Array.isArray(result.structuredContent) ? result.structuredContent : null;
|
|
3626
|
+
return { ...result, content: [{ type: 'text', text: note }, ...result.content], ...(sc || !result.isError ? { structuredContent: { ...(sc || {}), ignoredArguments: detail } } : {}) };
|
|
3627
|
+
}
|
|
3628
|
+
export function unknownArgsRefusal(args) {
|
|
3629
|
+
const r = args && typeof args === 'object' ? args[UNKNOWN_ARGS_REFUSED_SYM] : null;
|
|
3630
|
+
return r ? { content: [{ type: 'text', text: r.text }], structuredContent: { error: 'unknown_arguments', unknownArguments: r.detail }, isError: true } : null;
|
|
3614
3631
|
}
|
|
3615
3632
|
export function wellFormedServer(rawServer) {
|
|
3616
3633
|
guardUnknownArgs(rawServer);
|
|
@@ -3620,7 +3637,7 @@ export function wellFormedServer(rawServer) {
|
|
|
3620
3637
|
let safe = _wfWrapped.get(handler);
|
|
3621
3638
|
if (!safe) {
|
|
3622
3639
|
safe = async (args, extra) => {
|
|
3623
|
-
try { return wellFormedValue(withUnknownArgsNote(await handler(args, extra), args)); }
|
|
3640
|
+
try { const refused = unknownArgsRefusal(args); if (refused) return refused; return wellFormedValue(withUnknownArgsNote(await handler(args, extra), args)); }
|
|
3624
3641
|
catch (e) {
|
|
3625
3642
|
try { const m = wellFormedString(e?.message); if (e && m !== e.message) e.message = m; } catch {}
|
|
3626
3643
|
throw e;
|
|
@@ -4257,7 +4274,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
4257
4274
|
const extraKeys = undeclaredArgs(h, input);
|
|
4258
4275
|
if (extraKeys.length) {
|
|
4259
4276
|
const params = Object.entries(compactParams(h)).map(([k, v]) => `${k}: ${v}`).join(' | ');
|
|
4260
|
-
return { content: [{ type: 'text', text: `${undeclaredArgsText(n, extraKeys, h)}${params ? ` Expected params: ${params}` : ''}` }], isError: true };
|
|
4277
|
+
return { content: [{ type: 'text', text: `${undeclaredArgsText(n, extraKeys, h)}${params ? ` Expected params: ${params}` : ''}` }], structuredContent: undeclaredArgsStructured(extraKeys, h), isError: true };
|
|
4261
4278
|
}
|
|
4262
4279
|
if (h.inputSchema) {
|
|
4263
4280
|
const parsed = h.inputSchema.safeParse(input);
|
|
@@ -7426,14 +7443,14 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
7426
7443
|
}));
|
|
7427
7444
|
server.registerTool('list_youtube_comment_replies', {
|
|
7428
7445
|
title: 'Read the replies under one YouTube comment',
|
|
7429
|
-
description: 'Every reply under ONE comment on the connected channel, newest pages first via cursor: check that a reply the brand posted landed, or whether someone already answered a viewer before replying. Pass the top-level comment id from list_youtube_comments (each comment line shows its id and reply count). Read-only, free.',
|
|
7446
|
+
description: 'Every reply under ONE comment on the connected channel, newest pages first via cursor: check that a reply the brand posted landed, or whether someone already answered a viewer before replying. Pass the top-level comment id from list_youtube_comments (each comment line shows its id and reply count); a REPLY id is recognised and its whole thread is returned, with `note` and `requestedId` saying so. Read-only, free.',
|
|
7430
7447
|
inputSchema: { commentId: z.string().describe('the top-level comment id from list_youtube_comments'), limit: z.number().optional().describe('max replies per page, default 25, cap 100'), cursor: z.string().optional().describe('nextCursor from the previous page') },
|
|
7431
|
-
outputSchema: { commentId: z.string().optional(), count: z.number().optional(), replies: z.array(z.object({ id: z.string().optional(), author: z.string().optional(), authorChannelId: z.string().optional(), text: z.string().optional(), likes: z.number().optional(), at: z.string().optional(), editedAt: z.string().optional() })).optional(), nextCursor: z.string().optional() },
|
|
7448
|
+
outputSchema: { commentId: z.string().optional(), count: z.number().optional(), replies: z.array(z.object({ id: z.string().optional(), author: z.string().optional(), authorChannelId: z.string().optional(), text: z.string().optional(), likes: z.number().optional(), at: z.string().optional(), editedAt: z.string().optional() })).optional(), nextCursor: z.string().optional(), requestedId: z.string().optional(), resolvedFromReply: z.boolean().optional(), note: z.string().optional() },
|
|
7432
7449
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
7433
7450
|
}, wrap(async (a) => {
|
|
7434
7451
|
const d = await apiGet('/api/youtube/comment-replies', { commentId: a.commentId, ...(a.limit ? { limit: a.limit } : {}), ...(a.cursor ? { cursor: a.cursor } : {}) });
|
|
7435
7452
|
const rl = (d.replies || []).map(r => ` • ${r.author || 'someone'}${r.likes ? ` (${r.likes} like${r.likes === 1 ? '' : 's'})` : ''}: ${String(r.text || '').replace(/\s+/g, ' ').trim().slice(0, 400)}${r.at ? ` · ${r.at}` : ''}${r.id ? ` (id ${r.id})` : ''}`);
|
|
7436
|
-
return ok(`${d.count} repl${d.count === 1 ? 'y' : 'ies'} under comment ${d.commentId}:\n${rl.join('\n') || ' (none yet)'}${d.nextCursor ? `\nMore: pass cursor "${d.nextCursor}".` : ''}`, d);
|
|
7453
|
+
return ok(`${d.note ? `${d.note}\n` : ''}${d.count} repl${d.count === 1 ? 'y' : 'ies'} under comment ${d.commentId}:\n${rl.join('\n') || ' (none yet)'}${d.nextCursor ? `\nMore: pass cursor "${d.nextCursor}".` : ''}`, d);
|
|
7437
7454
|
}));
|
|
7438
7455
|
server.registerTool('reply_to_youtube_comment', {
|
|
7439
7456
|
title: 'Reply to a YouTube comment',
|
|
@@ -22480,14 +22497,14 @@ function memoryNoteVerdict(text) {
|
|
|
22480
22497
|
|
|
22481
22498
|
server.registerTool('recast_motion', {
|
|
22482
22499
|
title: 'Recast motion',
|
|
22483
|
-
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 + ONE photo of a person (saved, real or AI
|
|
22500
|
+
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 + ONE photo of a person (saved, real or AI), 4-30s -> seedance (Seedance 2.5 through our face library, 720p: the clip's scene, beats and sound kept, the WHOLE person swapped) when the clip is one of our renders or faceRoute is set; otherwise kling-edit (Kling 4.0 Edit, the second tier: edits the clip itself, 3-30s, 720p, 1080p by asking); several photos (one per person) -> h3 (H3 Max Recast, ≤4 photos, 5-30s); no person in the clip -> seedance. By name: kling-edit; wan = Wan 3.0 (≤15s, keeps the set); wan-prime; h3; kling = Kling Motion Control (ONE picture, its background becomes the set). 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.",
|
|
22484
22501
|
inputSchema: {
|
|
22485
22502
|
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),
|
|
22486
22503
|
images: z.array(z.string()).max(8).optional().describe('seedance/wan: more pictures, @Image2…: a character, product, outfit or place'),
|
|
22487
22504
|
video: z.string().describe('the reference video whose motion to re-perform'),
|
|
22488
22505
|
prompt: z.string().optional().describe("optional, e.g. 'same moves, new location: Tokyo at night'"),
|
|
22489
22506
|
engine: z.enum(['auto', 'kling-edit', 'h3', 'seedance', 'wan', 'wan-prime', 'kling']).optional().describe('default auto'),
|
|
22490
|
-
resolution: z.enum(['480p', '720p', '1080p']).optional().describe('
|
|
22507
|
+
resolution: z.enum(['480p', '720p', '1080p']).optional().describe('a person swap defaults to 720p (seedance or kling-edit); otherwise seedance and wan default 1080p; h3 720p or 1080p, default the clip’s own'),
|
|
22491
22508
|
orientation: z.enum(['video', 'image']).optional().describe("kling only: which aspect to keep: the video's (default) or the image's"),
|
|
22492
22509
|
faceRoute: z.enum(['face_lane']).optional().describe(FACE_ROUTE_CLIP_DESC),
|
|
22493
22510
|
dryRun: z.boolean().optional().describe('return the credits this exact job would hold and the engine auto picks, without rendering'),
|
|
@@ -22515,7 +22532,7 @@ function memoryNoteVerdict(text) {
|
|
|
22515
22532
|
// the engine; this wrapper only fills the brand's product photo. Held out of the default list on size (ON_DEMAND_TOOLS).
|
|
22516
22533
|
server.registerTool('recast_hook', {
|
|
22517
22534
|
title: 'Recast a viral hook with your subject',
|
|
22518
|
-
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; past 4, put one in words.
|
|
22535
|
+
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; past 4, put one in words. Person for person: Seedance 2.5 (720p) if the hook is our render or faceRoute is set, else Kling 4.0 Edit. FULL LENGTH: past its one pass (30 s) 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 multi-shot hook goes shot by shot); 'motion' only if asked (your person 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: no confirm returns the plan and credits; 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'}}].",
|
|
22519
22536
|
inputSchema: {
|
|
22520
22537
|
hook: z.string().describe('the viral hook: a post link or a video URL'),
|
|
22521
22538
|
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)"),
|
|
@@ -22527,7 +22544,8 @@ function memoryNoteVerdict(text) {
|
|
|
22527
22544
|
mode: z.enum(['auto', 'swap', 'motion']).optional(),
|
|
22528
22545
|
start: z.number().optional(), end: z.number().optional(),
|
|
22529
22546
|
prompt: z.string().optional().describe('extra direction for the render'),
|
|
22530
|
-
model: z.string().optional().describe("swap: omit for the default
|
|
22547
|
+
model: z.string().optional().describe("swap: omit for the default; a clip editor id (kling-4-edit, kling-o3-edit; hermoso_capabilities), or for one person swapped for another 'wan-3' (≤15 s) / 'minimax-h3-max-recast'"),
|
|
22548
|
+
faceRoute: z.enum(['face_lane']).optional().describe(FACE_ROUTE_CLIP_DESC),
|
|
22531
22549
|
resolution: z.enum(['480p', '720p', '1080p']).optional().describe("swap: the output tier, on an editor that prices by one (kling-4-edit 720p or 1080p); default the hook's own pixels"),
|
|
22532
22550
|
tier: z.enum(['pro', 'standard']).optional().describe('motion: the motion-transfer tier (default pro)'),
|
|
22533
22551
|
useBrand: z.boolean().optional().describe('false = no brand product photo; true with location/outfit and no subject = also swap in the brand product'),
|
|
@@ -22543,7 +22561,7 @@ function memoryNoteVerdict(text) {
|
|
|
22543
22561
|
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
|
|
22544
22562
|
if (a.useBrand !== false && !a.creator && !a.image && !subs.length && (!sceneOnly || a.useBrand === true)) { try { b = (await readStore('heist.brand.v1')) || {}; } catch { b = {}; } }
|
|
22545
22563
|
// 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)
|
|
22546
|
-
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.resolution ? { resolution: a.resolution } : {}), ...(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 } : {}) };
|
|
22564
|
+
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.resolution ? { resolution: a.resolution } : {}), ...(a.faceRoute === 'face_lane' ? { faceRoute: 'face_lane' } : {}), ...(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 } : {}) };
|
|
22547
22565
|
const r = await apiPost('/api/hooks/recast', body);
|
|
22548
22566
|
const p = r?.data || r;
|
|
22549
22567
|
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}` : ''}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.454",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "AI marketing agent and AI ad generator, run from your own AI agent. 933 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",
|