hermoso 0.1.424 → 0.1.426
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/client.mjs +14 -2
- package/mcp/tools.mjs +34 -19
- package/package.json +1 -1
package/mcp/client.mjs
CHANGED
|
@@ -137,7 +137,7 @@ export function signInMessage(status, body, msg, isSignedOut = signedOut()) {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// unwrap the {data}|{error} envelope; throw a clean Error (with .status) on failure
|
|
140
|
-
async function unwrap(res) {
|
|
140
|
+
async function unwrap(res, { read = false } = {}) {
|
|
141
141
|
let body = null;
|
|
142
142
|
try { body = await res.json(); } catch {}
|
|
143
143
|
if (!res.ok) {
|
|
@@ -148,6 +148,18 @@ async function unwrap(res) {
|
|
|
148
148
|
// `connectUrl` rides a not-connected 401 with the brand already in it, so a hint never rebuilds the link.
|
|
149
149
|
throw Object.assign(new Error(msg), { status: res.status, _viaApi: true, ...(body?.connector ? { connector: body.connector } : {}), ...(typeof body?.connectUrl === 'string' ? { connectUrl: body.connectUrl } : {}), ...(body?.meta?.videoChoice && typeof body.meta.videoChoice === 'object' ? { videoChoice: body.meta.videoChoice } : {}), ...(body?.metaAuthHold === true ? { metaAuthHold: true } : {}) }); // `videoChoice` rides a 402 for a video the caller expects and cannot afford (server videoChoiceFor) — wrap() spells its options out instead of a bare top-up line
|
|
150
150
|
}
|
|
151
|
+
// WHAT THIS CALL WAS CHARGED (2026-10-19). A paid route answers {data, creditsUsed}, and the unwrap below drops the
|
|
152
|
+
// sibling, so a synchronous paid tool's reply could never say what it cost. It is added to the running tool call's
|
|
153
|
+
// tally (toolCtx), which wrap() in tools.mjs states once at the end of the reply. Free routes answer 0 or nothing.
|
|
154
|
+
// Two shapes, both measured: a sibling {data, creditsUsed} (the research routes) and creditsUsed INSIDE data (generate_voice
|
|
155
|
+
// and the other generate routes; the first live run of this tally printed nothing on the CLI because of it). Inside-data
|
|
156
|
+
// counts only on a write: a GET that returns a record may carry a past charge, which is not this call's.
|
|
157
|
+
try {
|
|
158
|
+
const sib = body && typeof body === 'object' ? Number(body.creditsUsed) : NaN;
|
|
159
|
+
const inner = !read && body && body.data && typeof body.data === 'object' && !Array.isArray(body.data) ? Number(body.data.creditsUsed) : NaN;
|
|
160
|
+
const c = Number.isFinite(sib) ? sib : inner;
|
|
161
|
+
const st = toolCtx.getStore(); if (st && Number.isFinite(c) && c > 0) st.apiCharged = (st.apiCharged || 0) + c;
|
|
162
|
+
} catch {}
|
|
151
163
|
return body && Object.prototype.hasOwnProperty.call(body, 'data') ? body.data : body;
|
|
152
164
|
}
|
|
153
165
|
|
|
@@ -205,7 +217,7 @@ export async function apiGet(p, query) {
|
|
|
205
217
|
const clean = query && Object.fromEntries(Object.entries(query).filter(([, v]) => v !== undefined && v !== null && v !== ''));
|
|
206
218
|
const qs = clean && Object.keys(clean).length ? '?' + new URLSearchParams(clean).toString() : '';
|
|
207
219
|
const res = await fetchRead(`${API_BASE}${withBrandScope(`${p}${qs}`)}`, { headers: headers() });
|
|
208
|
-
return unwrap(res);
|
|
220
|
+
return unwrap(res, { read: true });
|
|
209
221
|
}
|
|
210
222
|
|
|
211
223
|
// ── A TRANSPORT FAILURE IS NOT A DEFECT, AND IT MUST NOT READ LIKE ONE (2026-08-31) ────────────────────────────
|
package/mcp/tools.mjs
CHANGED
|
@@ -517,8 +517,15 @@ const wrap = (fn) => {
|
|
|
517
517
|
// because that is what the caller experienced, whoever authored the refusal.
|
|
518
518
|
const _t0 = Date.now();
|
|
519
519
|
try {
|
|
520
|
-
const
|
|
520
|
+
const _st = { tool: _tool };
|
|
521
|
+
const r = await toolCtx.run(_st, () => fn(args, extra));
|
|
521
522
|
try { recordToolOutcome(_tool, { ok: !r?.isError, ms: Date.now() - _t0 }); } catch {}
|
|
523
|
+
// the charge, once, on every paid reply that did not already say it (chargeNote above)
|
|
524
|
+
try {
|
|
525
|
+
const _c = Math.round((_st.apiCharged || 0) + (_st.jobCharged || 0));
|
|
526
|
+
const _txt = Array.isArray(r?.content) ? r.content.find((b) => b && b.type === 'text' && typeof b.text === 'string') : null;
|
|
527
|
+
if (_c > 0 && !_st.chargeSaid && _txt && !CHARGED_RE.test(_txt.text)) _txt.text += r?.structuredContent?.stillRendering ? `\nCharged ${_c} credit${_c === 1 ? '' : 's'} so far; the render's own charge is reported by get_job when it finishes.` : chargeSentence(_c);
|
|
528
|
+
} catch {}
|
|
522
529
|
return r;
|
|
523
530
|
}
|
|
524
531
|
catch (e) {
|
|
@@ -631,6 +638,7 @@ async function awaitRenderJob(job) {
|
|
|
631
638
|
if (waitMs <= 0) return { jobId: job.id, url: null, stillRendering: true, raw: null };
|
|
632
639
|
try {
|
|
633
640
|
const { result, job: doneJob } = await pollJob(job.id, { timeoutMs: waitMs });
|
|
641
|
+
{ const c = Number(doneJob?.chargedCredits ?? result?.creditsUsed); const st = toolCtx.getStore(); if (st && Number.isFinite(c) && c > 0) st.jobCharged = (st.jobCharged || 0) + c; } // the tool call's tally (chargeNote / wrap)
|
|
634
642
|
const url = abs(result?.video || result?.image || result?.url);
|
|
635
643
|
// creditsUsed AND the delivered geometry ride out here deliberately. The widget's own registered
|
|
636
644
|
// description promises "the model that rendered it and credits spent", and the pill only ever appeared
|
|
@@ -664,8 +672,15 @@ const renderPayload = (r) => (r?.raw ?? jobResult(r) ?? null);
|
|
|
664
672
|
// WHAT A FINISHED RENDER WAS CHARGED (2026-10-19). Pre-run prices are estimates now, so the reply that delivers a render
|
|
665
673
|
// says the real charge: the job's own chargedCredits (every leg it billed), else the worker's settle. Rides switchNote
|
|
666
674
|
// because switchNote is already on every render tool's delivery sentence, inline and resumed via get_job alike.
|
|
675
|
+
// ONE SENTENCE FOR EVERY PAID TOOL (2026-10-19: recast_motion, and most job tools, delivered with no charge at all, because
|
|
676
|
+
// only the replies that happened to carry switchNote said it). The tool call keeps a tally (toolCtx): every paid route's
|
|
677
|
+
// creditsUsed (client.mjs unwrap) and every job it waited for (awaitRenderJob). chargeNote states the job's charge plus
|
|
678
|
+
// the call's other paid requests (a planning call before a render) and marks it said; wrap() states the tally on any
|
|
679
|
+
// reply that has not, so a tool needs no line of its own and cannot forget one.
|
|
680
|
+
const chargeSentence = (c) => `\nCharged ${c} credit${c === 1 ? '' : 's'} (the actual cost of this run).`;
|
|
681
|
+
const CHARGED_RE = /\nCharged \d[\d,]* credits? \(/;
|
|
667
682
|
const chargedOf = (r) => { const v = r?.chargedCredits ?? r?.creditsUsed ?? renderPayload(r)?.creditsUsed ?? r?.result?.creditsUsed; return Number.isFinite(+v) && +v > 0 ? Math.round(+v) : null; };
|
|
668
|
-
const chargeNote = (r) => { const
|
|
683
|
+
const chargeNote = (r) => { const st = toolCtx.getStore(); const tally = Math.round((st?.jobCharged || 0) + (st?.apiCharged || 0)); const c = tally > 0 ? tally : (chargedOf(r) || 0); if (!(c > 0)) return ''; if (st) st.chargeSaid = true; return chargeSentence(c); }; // the tally when this call has one (it already holds the job's charge and any paid request beside it), else the record handed in (get_job reads a job it did not wait for)
|
|
669
684
|
const switchNote = (r) => { const n = renderPayload(r)?.modelNote || r?.modelNote; return (n ? `\n⚠ ${n}` : '') + chargeNote(r); };
|
|
670
685
|
// THE VISION-QA READ-BACK (2026-08-03). The server WATCHES every generatively-rendered clip before it reports it
|
|
671
686
|
// done — free ffmpeg cadence/freeze probes plus ONE vision call over frames sampled at the hook, either side of
|
|
@@ -18388,9 +18403,9 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
18388
18403
|
// local path on the CLI (sent as bytes, stored once by the server). An id or path that resolves to nothing is refused
|
|
18389
18404
|
// by name, free, before anything is planned or rendered.
|
|
18390
18405
|
const INSERTS_Z = z.array(z.object({
|
|
18391
|
-
image: z.string().describe('
|
|
18392
|
-
start: z.number().describe('second
|
|
18393
|
-
end: z.number().describe('second it ends
|
|
18406
|
+
image: z.string().describe('an https or upload_file URL, a Library id (L0, L1…) or a local path on the CLI'),
|
|
18407
|
+
start: z.number().describe('second it appears'),
|
|
18408
|
+
end: z.number().describe('second it ends (≥0.5 s after start)'),
|
|
18394
18409
|
})).max(8);
|
|
18395
18410
|
const insertsResolved = async (list) => {
|
|
18396
18411
|
if (!Array.isArray(list)) return undefined;
|
|
@@ -18427,7 +18442,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
18427
18442
|
language: z.string().optional().describe('output language for the ad copy (e.g. Spanish) — default English'),
|
|
18428
18443
|
talent: z.enum(['auto', 'creator', 'product_only']).optional().describe("who is on camera; omit to follow the format. Say 'someone new' in product to skip reusing a saved creator."),
|
|
18429
18444
|
draft: z.object({ model: z.string(), durationSeconds: z.number() }).optional().describe("ONLY after a video refusal that offered a light draft: the {model, durationSeconds} it named. The plan is then authored to that length and priced on that model. Never invent one — a video the account cannot cover is refused BEFORE planning with the three options (image / add credits / this draft when one fits), and the user chooses."),
|
|
18430
|
-
inserts: INSERTS_Z.optional().describe("VIDEO ONLY: the user's
|
|
18445
|
+
inserts: INSERTS_Z.optional().describe("VIDEO ONLY: the user's own pictures (screenshots, a page, a chart) shown FULL-SCREEN at these seconds, the voice running underneath. The plan keeps those shots free of screens, never uses the pictures as references, and returns them as creative.inserts for render_ad to lay in (about 2 credits). At most 8, never two at once; one the planned length has no room for is left out and the reply says so."),
|
|
18431
18446
|
},
|
|
18432
18447
|
outputSchema: {
|
|
18433
18448
|
format: z.string().optional().describe("the resolved creative format — 'image' or 'video'"),
|
|
@@ -18567,7 +18582,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
18567
18582
|
imageSize: z.string().optional().describe('pixel-size preset for models that support it: 1K/2K, and 4K on the models hermoso_capabilities lists with a 4K imageSize price (a 4K ask on any other model is refused, free) — omit for the default'),
|
|
18568
18583
|
fixLabel: z.boolean().optional().describe('default true: when the saved brand\'s product photo rides in this render, the product\'s label on the finished image is READ and compared with the photo, and re-printed from the photo at close range ONLY if it came out wrong (a label that is already right costs only the check, a credit or two; a re-print adds about ten). The reply says whether the label was checked, fixed or left as rendered (`labelPass`). Pass false when the user wants the packaging left exactly as generated: nothing is checked or re-printed.'),
|
|
18569
18584
|
logoPlacement: z.enum(['auto', 'overlay', 'in_scene', 'none']).optional().describe('overlay: real logo file laid flat. in_scene: on something in the scene, from the file, checked. auto (default): from the prompt'),
|
|
18570
|
-
mask: z.string().optional().describe('MASKED EDIT — change ONE region
|
|
18585
|
+
mask: z.string().optional().describe('MASKED EDIT — change ONE region and keep the rest: a mask (path or URL, any size) for refImages[0]. TRANSPARENT = change, or on a mask with no transparency WHITE = change, black = keep; the reply says which it read. The mask guides rather than stencils, so the change can blend a little past its edge. Runs on gpt-image-2.5 (`refs.mask` in hermoso_capabilities): leave `model` empty or name it; another named model is refused, free. Needs refImages; keeps the source image\'s frame, so aspectRatio is not applied.'),
|
|
18571
18586
|
},
|
|
18572
18587
|
outputSchema: {
|
|
18573
18588
|
image: z.string().optional().describe('the served absolute URL of the finished image'),
|
|
@@ -18833,12 +18848,12 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
18833
18848
|
description: 'Use this when the user wants a finished video ad for their brand. RECOMMENDED for finished video ADS: render a plan_ad concept through the SAME quality pipeline as the Hermoso web Studio — timed shot list, exact/clean speech (no garbled words), text composited in post (never model-painted), an optional brand end card (only when the user asks), no music unless asked, real product references. UGC and cinematic plans are rendered from a painted STORYBOARD board (cleaned, then bound first, ahead of the creator and the product); a cinematic spot paints a location anchor first (and a hero when someone is on camera). A product-only commercial renders from the real product photo by default; board:true or foundations:\'choose\' adds a product identity sheet, a four-up moodboard and a board. Pass plan_ad’s full structured output as `creative`. Honors the plan’s render_plan structure/duration: a storyboard that FITS ONE CLIP OF THE RENDER MODEL renders as a single continuous pass; anything longer automatically renders as STITCHED ACTS (the fewest balanced clips, each at most one model clip) — never time-compressed into one clip. That threshold is the render model’s own maximum, not a fixed number: most models cap a clip at 15s and the longest-clip one goes to 30s, so use dryRun:true to see the act split this plan will actually get, for free, before spending. CAST A SAVED CREATOR with `creator` so the SAME person stars in this ad as in the last one (list_creators is the roster) — otherwise every render invents a new face. Renders take 1–3 min; keep polling get_job if it returns still-rendering. The user’s own pictures that plan_ad was given (creative.inserts) are laid in full-screen at their seconds by the same job, or pass `inserts` here. Spends credits.',
|
|
18834
18849
|
inputSchema: {
|
|
18835
18850
|
creative: z.object({}).passthrough().describe('the FULL structured output of plan_ad (must contain video_storyboard)'),
|
|
18836
|
-
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
|
|
18851
|
+
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 (casting skips the portrait we would otherwise generate). 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),
|
|
18837
18852
|
faceRoute: z.enum(['face_lane']).optional().describe(FACE_ROUTE_DESC),
|
|
18838
18853
|
model: z.string().optional().describe('video model id from hermoso_capabilities (default: the engine the router picks for the plan: seedance-2.5 for every ad, a person talking to camera included). Naming one is a DELIBERATE pick — the server asks before ever swapping it (no silent fallback)'),
|
|
18839
|
-
durationSeconds: z.number().optional().describe('total ad length in seconds
|
|
18854
|
+
durationSeconds: z.number().optional().describe('total ad length in seconds, 4–180 (clamped). Omit to keep the plan’s own length, almost always right: this only RE-TIMES the authored board; to change the ad’s length, re-run plan_ad with durationSeconds. A length that fits ONE clip renders in one pass; longer is stitched from acts of the model’s clip maximum (15s on most, 30s on the longest-clip one), remainder last. dryRun:true shows the split free.'),
|
|
18840
18855
|
aspectRatio: z.string().optional().describe('output aspect ratio, e.g. 9:16 (default) / 1:1 / 16:9'),
|
|
18841
|
-
resolution: z.enum(['480p', '720p', '1080p', '4k']).optional().describe("'1080p' default
|
|
18856
|
+
resolution: z.enum(['480p', '720p', '1080p', '4k']).optional().describe("'1080p' default; '480p'/'720p' cheaper drafts, '4k' premium (more credits). A model renders only the tiers in its own `resolutions` list in hermoso_capabilities (the 30s model tops out at 720p); a tier it lacks renders at its best one without a word, so check before promising 1080p or 4k."),
|
|
18842
18857
|
captions: z.boolean().optional().describe('burn the plan\'s per-scene on-screen words as caption pills. DEFAULT FALSE on every recipe — set true ONLY when the user asks for on-screen text or captions; no recipe turns them on by itself'),
|
|
18843
18858
|
endCard: z.boolean().optional().describe('append the branded end card. DEFAULT FALSE on every recipe — set true ONLY when the user asks for an end card (a clone of a video that had none should not grow one)'),
|
|
18844
18859
|
music: z.union([z.boolean(), z.string()]).optional().describe("music bed: OFF unless asked. true = the plan's own music line; or the bed in words ('lo-fi jazz, brushed drums'); false = none"),
|
|
@@ -18851,7 +18866,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
18851
18866
|
ttsVoice: z.string().optional().describe('voiceover voice name (e.g. Rachel / George) when the plan voices over'),
|
|
18852
18867
|
dryRun: z.boolean().optional().describe('return the routing decision (single pass vs stitched acts, resolved model + act lengths) and the estimated credits the real render reserves, WITHOUT submitting a render — free, nothing charged'),
|
|
18853
18868
|
allowGenericProduct: z.boolean().optional().describe('proceed even though this brand has NO product photo on file and the ad features a product — the packaging will be INVENTED. Only pass true after telling the user that and hearing they are fine with a generic stand-in'),
|
|
18854
|
-
inserts: INSERTS_Z.optional().describe("the user's
|
|
18869
|
+
inserts: INSERTS_Z.optional().describe("the user's own pictures laid FULL-SCREEN over the finished video at these seconds, sound running underneath (about 2 credits). Omit to use plan_ad's creative.inserts; [] renders without them. plan_ad with inserts writes the board around them."),
|
|
18855
18870
|
},
|
|
18856
18871
|
outputSchema: {
|
|
18857
18872
|
...JOB_OUT,
|
|
@@ -19317,19 +19332,19 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
19317
19332
|
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'),
|
|
19318
19333
|
creator: z.string().optional().describe('STAR A SAVED CREATOR in this clip — their id from list_creators, or the name you know them by, or a PRESET AI creator from list_creators presets (exact name or id; free, no generation). Their saved portrait rides first among the references as the on-camera person, with their saved consent, exactly as render_ad casts them; a real person saved from a photo keeps their real face on camera. An unknown name is refused by name, nothing charged.'),
|
|
19319
19334
|
angles: z.boolean().optional().describe('OPTIONAL, OFF BY DEFAULT: with `creator`, also send the extra views that creator already has saved (their pose plates, up to 2) beside their portrait. A test showed no visible improvement over the portrait alone, and each extra view adds about 25 s before the render starts, so leave it off unless asked. Views are skipped when the face library is busy, and the render always goes ahead on the portrait.'),
|
|
19320
|
-
refImages: z.array(z.string()).optional().describe('SEVERAL reference images (
|
|
19321
|
-
refVideo: z.string().optional().describe("URL of an existing video to EDIT rather than generate from scratch — the clip is transformed per your prompt,
|
|
19335
|
+
refImages: z.array(z.string()).optional().describe('SEVERAL reference images (paths or URLs: a person, products, a place) that must all appear. A model uses up to its `refs.max` in hermoso_capabilities (one-image models take the first); with `refs.promptAddressed`, name them Image 1, Image 2… in order. minimax-h3-max-ref takes 9 and keeps each as a reference, not a first frame.'),
|
|
19336
|
+
refVideo: z.string().optional().describe("URL of an existing video to EDIT rather than generate from scratch — the clip is transformed per your prompt, keeping the SOURCE clip’s aspect ratio and, on models hermoso_capabilities marks `sourceLength`, its LENGTH (their `durations` are the per-second price ladder; a durationSeconds sent is reported as unused). 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."),
|
|
19322
19337
|
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.'),
|
|
19323
19338
|
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.'),
|
|
19324
|
-
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
|
|
19325
|
-
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.
|
|
19339
|
+
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 add up to a length the model renders. Replaces `prompt`. Only models with multiShot true in hermoso_capabilities; a shot prompt over that model’s shotPromptMaxChars (Kling 3.0: 512) is refused free, never cut.'),
|
|
19340
|
+
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. Omit model for the certified default.'),
|
|
19326
19341
|
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.'),
|
|
19327
|
-
durationSeconds: z.number().optional().describe('length of THIS ONE clip in seconds
|
|
19342
|
+
durationSeconds: z.number().optional().describe('length of THIS ONE clip in seconds: one of the CHOSEN model’s listed durations in hermoso_capabilities (they differ per model, 4–8s on the short ones up to 30s). One continuous generation, so a longer ask than the model’s longest clip is REFUSED, nothing charged (never quietly truncated). Past ~15s NAME a long-clip model in `model`. For a longer spot use plan_ad with durationSeconds then render_ad, which stitches acts (on a 15s model, 40s = 15+15+10).'),
|
|
19328
19343
|
aspectRatio: z.string().optional().describe("default '9:16'"),
|
|
19329
19344
|
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; its default per job (jobEngines in hermoso_capabilities, from watched side-by-side tests) is seedance-2.5 for product, lifestyle, a person talking to camera and 30s single takes (cheaper engines broke the product there). A model the user picks is kept; never switch it'),
|
|
19330
19345
|
faceRoute: z.enum(['face_lane']).optional().describe(FACE_ROUTE_DESC),
|
|
19331
|
-
resolution: z.enum(['480p', '720p', '1080p', '4k']).optional().describe("'1080p' default
|
|
19332
|
-
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
|
|
19346
|
+
resolution: z.enum(['480p', '720p', '1080p', '4k']).optional().describe("'1080p' default; '480p'/'720p' cheaper drafts, '4k' premium (more credits). A model renders only the tiers in its own `resolutions` list in hermoso_capabilities; a tier it lacks renders at its best one without a word, so check before promising 1080p or 4k."),
|
|
19347
|
+
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 (orbit = a quarter turn, the default; orbit_half / orbit_full = a half turn / a turntable). Only minimax-h3-max-camera renders one: with model omitted it is picked; any other named model is refused, nothing charged. Needs refImage.'),
|
|
19333
19348
|
cameraTrajectory: z.array(z.object({
|
|
19334
19349
|
time: z.number().min(0).max(1).describe('when this pose is reached, 0 = start of the clip, 1 = end'),
|
|
19335
19350
|
azimuth: z.number().describe('horizontal angle around the subject in degrees (0 = where the still was taken; the sign turns the camera the other way; at most 32 full turns of total travel)'),
|
|
@@ -19338,7 +19353,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
19338
19353
|
})).min(2).max(12).optional().describe('Your own ordered camera path, 2 to 12 keyframes, for the camera-controls model only (same rule as cameraMove; overrides it). The first pose is held until its time and the last pose is held to the end. A value outside these bounds is refused by name, nothing charged.'),
|
|
19339
19354
|
ttsScript: z.string().optional().describe('voiceover script to speak'),
|
|
19340
19355
|
ttsVoice: z.string().optional().describe('voice name, e.g. Rachel / George'),
|
|
19341
|
-
musicMood: z.string().optional().describe('
|
|
19356
|
+
musicMood: z.string().optional().describe('the mood of the music bed (upbeat / calm / warm / epic / tense / playful / elegant / hype / chill / dramatic; default warm). It does not decide WHETHER there is one: a clip with no audio track (a "silent" model in hermoso_capabilities, or one that came back mute) gets a bed composed and CHARGED automatically at explainerMusicCredits. audio:false = silent, no bed, no charge.'),
|
|
19342
19357
|
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.'),
|
|
19343
19358
|
},
|
|
19344
19359
|
outputSchema: { ...JOB_OUT,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.426",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "AI marketing agent and AI ad generator, run from your own AI agent. 922 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",
|