hermoso 0.1.292 → 0.1.294

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 +18 -4
  2. package/package.json +1 -1
package/mcp/tools.mjs CHANGED
@@ -113,6 +113,10 @@ const channelOutcomeLine = (res) => {
113
113
  const stillMsg = (r, widget = hostRendersWidgets()) => widget
114
114
  ? `NOT FINISHED. Job ${r.jobId} is still rendering and there is no video yet. The Hermoso card above is already polling and will show the finished video in place the moment it lands, so do NOT call get_job and do NOT re-render. Reply with ONE short line saying it is rendering and will appear in the card above. Do NOT say it is done, ready, rendered or finished, and do NOT describe the video — you have not seen it.`
115
115
  : `Still rendering — job ${r.jobId}. This is NORMAL: video renders take 1–3 minutes and each get_job call waits up to ~45s, so it can take several calls. Keep calling get_job with this id until status is done or error — do NOT ask the user whether to keep waiting, and do NOT re-fire the render on another model (that double-charges). Only surface a problem after ~6 minutes of polling. Until get_job reports done you have NO video: do NOT say it is finished and do NOT describe it.`;
116
+ // A TIMELINE EDIT'S SELF-CRITIQUE, one sentence for both places it can arrive: edit_timeline's own reply when the render
117
+ // finished inside the wait, and get_job's when it did not (a prod timeline takes ~3 minutes, past the hosted 45 s wait,
118
+ // so on a hosted MCP the review ONLY ever arrives through get_job). Empty for every other job.
119
+ const timelineReviewText = (rv) => rv ? `\nREVIEW (${rv.verdict || 'unread'}${rv.score != null ? `, ${rv.score}/10` : ''}${rv.linked === false ? ', NOT LINKED: the two clips read as unrelated; say so and offer a follow clip made for the hook (a brief they record, or one generated with the cost quoted first)' : rv.linked ? ', linked' : ''}): ${(rv.issues || []).map((x) => `seam ${x.seam}: ${x.problem} → ${x.fix}`).join(' | ') || 'no issues'}. ${rv.verdict === 'amateur' || rv.verdict === 'ok' ? 'Fix what it names and re-run with the same sources (twice at most) before presenting it.' : 'Look at the seam frames too before presenting it.'}` : '';
116
120
  const okVideo = async (text, r) => {
117
121
  if (r?.stillRendering) return ok(stillMsg(r), r); const p = r?.url ? await videoPosterBlock(r.url) : null; const t = text + geoLine(r) + qaLine(r); return { content: [{ type: 'text', text: p ? t + '\n(first frame attached — open the URL for the full video)' : t }, ...(p ? [p] : [])], structuredContent: r ?? {} }; };
118
122
 
@@ -17343,7 +17347,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
17343
17347
  return okVideo(`Edited video ready: ${r.url}${Array.isArray(r?.raw?.applied) ? ` (${r.raw.applied.join(', ')})` : ''}${Array.isArray(r?.raw?.notes) && r.raw.notes.length ? `\nNOTE: ${r.raw.notes.join('; ')}` : ''} [job ${r.jobId}]`, r);
17344
17348
  }));
17345
17349
 
17346
- // THE OPEN EDIT PRIMITIVE (2026-09-24). Dave: "I hate being rigid, the whole point of this is being able to do basically
17350
+ // THE OPEN EDIT PRIMITIVE (2026-09-24). The owner: "I hate being rigid, the whole point of this is being able to do basically
17347
17351
  // anything they want", and: "their super smart AI agent should have hermoso, access all our tools and be able to make
17348
17352
  // what it thinks is best for the user". So there is no list of named transitions: the caller COMPOSES any edit from
17349
17353
  // segments + keyframes + overlay HTML, and the server (lib/timeline.mjs) validates every value and compiles the
@@ -17360,8 +17364,9 @@ function buildTools(rawServer, opts = {}, sink = null) {
17360
17364
  + "A VIRAL HOOK + THEIR PRODUCT: start from the hook. A clip matched to an unrelated hook never reads as one video, so write the clip AFTER the hook for it: a linking script + shot brief (the first line answers the hook, e.g. 'still waiting for the egg to land... anyway, come check out our restaurant'; what to film so it follows on; 5-15 s; then the pitch). The user records it, or you generate it (render_ad / generate_video, cost quoted first, on their OK); then join here: the hook with out:'payoff' and audio.tail:'payoff', then their clip. Match an existing unrelated clip only if they insist. A {generate:{prompt, seconds 3-8}} segment (a generated transition-only shot, paid, postEditTimeline) is never suggested; build it only when they explicitly ask for one. "
17361
17365
  + "LINK FIRST: an effect alone never connects two unrelated clips; the link comes from what is in the frames. (1) Match cut, the default: video_frames (with its MOTION readout) on the hook's last second and across the other clip; pick the out-point AND the in-point (in: seconds, not always 0) where a motion direction, a screen position or size, a shape, a surface, a gesture or a gaze carries across, then ride the effect on that shared motion. (2) Its host names the hook in the first line. If the two share nothing, say so and offer the follow clip made for the hook. "
17362
17366
  + "PRO, NOT IMOVIE: ease every curve (never linear on a move); keep the picture filling the frame through a move (scale up while it moves: two frames sliding side by side with a seam is the amateur tell); hide the handoff under the fastest, blurriest frames; carry direction into the next shot; cut on motion; end every effect cleanly; 0.2-0.6 s in total; a sound whose peak lands on the handoff (sfx whoosh at handoff minus 0.45 s, or the hook's own payoff sound). Moving segments get a real shutter blur automatically (motionBlur). "
17367
+ + "MATCH EVERY SEAM, hard cuts too, cheapest first: exposure (skin and midtones within ~5%), white balance (skin first; a green cast reads worse than a warm one), black level, then grain UP, never down (post_edit grain on the result so the clean clip meets the grainy one; never smooth the grainy one), motion blur (cut where both move least, or mblur the crisp side only while it moves), subject size and headroom (scale it, never a jump from a third of the frame to two thirds), sound (a 0.25-0.5 s J/L-cut, never a sonic wall). Grade with the segment's exposure / contrast / saturation. The plainest thing that links wins: a straight cut on action beats a decorative effect; over 0.5 s is too long in anything under 20 s; never flash more than 3 times a second. "
17363
17368
  + "RECIPES (c = the cut second, adapt freely): whip pan: A over its last 0.22 s x 0 to -0.22, scale 1 to 1.35, mblur 0 to 220, all ease in; B overlap 0.08, opacity 0 to 1 over 0.08, x 0.22 to 0, scale 1.35 to 1, mblur 220 to 0, all ease out over 0.3 s; whoosh at c-0.45. Zoom through: A over its last 0.35 s scale 1 to 3 ease in anchored on the object, blur 0 to 10; B overlap 0.12, opacity 0 to 1, scale 1.5 to 1 and blur 10 to 0 ease out over 0.4 s. Cut on action: A out ON the motion, B scale 1.08 to 1 ease out over 0.25 s, audio.lead 0.2. Speed ramp: speed [{src:t0,v:1},{src:t0+0.25,v:0.3}] then [{src:t1,v:0.3},{src:t1+0.1,v:2}] into the cut. Circle wipe: B overlap 0.5 + overlays [{mode:'mask', segment:1, start, end, html: a white div whose clip-path circle grows via @keyframes}]. "
17364
- + "SELF-CRITIQUE: the reply carries a vision REVIEW of each seam (pro / ok / amateur, linked or not, with fixes; about 2 credits, review:false skips it) and the seam frames. When it says ok or amateur, fix what it names and re-run the same sources (twice at most) before presenting; on a re-run give a generated segment {src: its URL, between: true} so it is not paid for twice. "
17369
+ + "SELF-CRITIQUE: the reply carries a vision REVIEW of each seam (pro / ok / amateur, linked or not, with fixes; about 2 credits, review:false skips it) and the seam frames. When it says ok or amateur, fix what it names and re-run the same sources (twice at most) before presenting; on a re-run give a generated segment {src: its URL, between: true} so it is not paid for twice. Then look at the WHOLE result once with video_frames, not only the seams: the first frame is not black or frozen, no dead air over ~0.3 s at the head, no lone black, flash or repeated frame at a cut, and nothing static for more than ~4-5 s (recut it or add a re-hook). "
17365
17370
  + "FOLLOW-UPS ('cut earlier', 'no splat', 'whip pan instead', 'use the second hook') re-run this with the SAME sources and the one change; the result echoes the resolved timeline (e.g. the found payoff cut) to edit from. Refusals are free and name the field.",
17366
17371
  inputSchema: {
17367
17372
  videoUrl: z.string().optional().describe("the video 'this' refers to (optional when every segment names its own src)"),
@@ -17404,10 +17409,13 @@ function buildTools(rawServer, opts = {}, sink = null) {
17404
17409
  // THE SELF-CRITIQUE comes back WITH the render: the verdict, the fixes, and the seam frames the reviewer saw, inline,
17405
17410
  // so the calling agent can see its own transition before it shows the user anything.
17406
17411
  const rv = r?.raw?.review;
17407
- const rvText = rv ? `\nREVIEW (${rv.verdict || 'unread'}${rv.score != null ? `, ${rv.score}/10` : ''}${rv.linked === false ? ', NOT LINKED: the two clips read as unrelated; say so and offer a follow clip made for the hook (a brief they record, or one generated with the cost quoted first)' : rv.linked ? ', linked' : ''}): ${(rv.issues || []).map((x) => `seam ${x.seam}: ${x.problem} → ${x.fix}`).join(' | ') || 'no issues'}. ${rv.verdict === 'amateur' || rv.verdict === 'ok' ? 'Fix what it names and re-run with the same sources (twice at most) before presenting it.' : 'Look at the seam frames too before presenting it.'}` : '';
17412
+ // STILL RENDERING AFTER THE WAIT (a prod timeline takes ~3 minutes; the hosted wait is 45 s): the job id and the
17413
+ // polling instruction, plus where the self-critique will arrive, so the caller neither re-renders nor presents blind.
17414
+ if (r?.stillRendering && !hostRendersWidgets()) return ok(`${stillMsg(r)} A timeline edit usually takes about 3 minutes. When get_job(${r.jobId}) reports done, its answer carries the SEAM REVIEW and the seam frames: read them before you present the edit, and fix + re-run (same sources) if it says ok or amateur.`, r);
17415
+ const rvText = timelineReviewText(rv);
17408
17416
  const res = await okVideo(`Edited video ready: ${r.url}${Array.isArray(r?.raw?.notes) && r.raw.notes.length ? `\nNOTE: ${r.raw.notes.join('; ')}` : ''}${tl ? `\nRESOLVED: ${tl.segments.map((sg) => `[${sg.i}] ${sg.in != null ? `${sg.in}-${sg.out}s` : `${sg.seconds}s`} @${sg.at}s`).join(', ')} (${tl.seconds}s)` : ''}${gen}${rvText} [job ${r.jobId}]`, r);
17409
17417
  const sheet = rv?.seamSheets?.[0]?.url ? await imageBlock(abs(rv.seamSheets[0].url)) : null;
17410
- if (sheet && Array.isArray(res.content)) res.content.push({ type: 'text', text: `Seam 1 frames (${rv.seamSheets[0].at}s, 15 fps, left to right):` }, sheet);
17418
+ if (sheet && Array.isArray(res.content)) res.content.push({ type: 'text', text: `Seam 1 frames (${rv.seamSheets[0].at}s${rv.seamSheets[0].end > rv.seamSheets[0].at ? `-${rv.seamSheets[0].end}s` : ''}, ${rv.seamSheets[0].fps || 15} fps, left to right):` }, sheet);
17411
17419
  return res;
17412
17420
  }));
17413
17421
 
@@ -17784,6 +17792,12 @@ function buildTools(rawServer, opts = {}, sink = null) {
17784
17792
  const wireText = `${text}\n${JSON.stringify({ id: j.id, status: j.status, progress: j.progress ?? null, url: url || null, error: j.error || null,
17785
17793
  model: j.model || rawJ.model || null, poster: abs(rawJ.poster) || null, creditsUsed: j.creditsUsed ?? null,
17786
17794
  deliveredWidth: rawJ.deliveredWidth ?? null, deliveredHeight: rawJ.deliveredHeight ?? null })}`;
17795
+ if (j.status === 'done' && res?.video && res?.review) { // a finished TIMELINE edit: its seam review + frames, as edit_timeline would have shown them
17796
+ const out = await okVideo(`${wireText}${timelineReviewText(res.review)}`, { ...j, url });
17797
+ const sheet = res.review?.seamSheets?.[0]?.url ? await imageBlock(abs(res.review.seamSheets[0].url)) : null;
17798
+ if (sheet && Array.isArray(out.content)) out.content.push({ type: 'text', text: `Seam 1 frames (${res.review.seamSheets[0].at}s${res.review.seamSheets[0].end > res.review.seamSheets[0].at ? `-${res.review.seamSheets[0].end}s` : ''}, ${res.review.seamSheets[0].fps || 15} fps, left to right):` }, sheet);
17799
+ return out;
17800
+ }
17787
17801
  if (j.status === 'done' && res?.video) return okVideo(wireText, { ...j, url }); // resumed video → same inline poster as a direct return
17788
17802
  if (j.status === 'done' && res?.image) { const img = await imageBlock(url); return { content: [{ type: 'text', text: wireText }, ...(img ? [img] : [])], structuredContent: { ...j, url } }; }
17789
17803
  return ok(wireText, { ...j, url });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.292",
3
+ "version": "0.1.294",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
5
  "description": "Marketing on autopilot, run from your own AI agent. 863 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 and ChatGPT Ads, plus product feeds in Google Merchant Center. PUBLISHING AND SCHEDULING: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky and Telegram. 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",