goatedit-mcp 1.1.19 → 1.1.21

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.
@@ -20043,6 +20043,16 @@ var BASE_TOOLS = [
20043
20043
  },
20044
20044
  method: "getEditLog"
20045
20045
  },
20046
+ {
20047
+ name: "get_operation",
20048
+ description: 'Where an edit stands after its call timed out or the connection dropped: queued, running, done (with its reply) or failed, by the opId the error gave. "unknown" means the tab never got it, so sending it again is safe. To retry, repeat the call with that same opId: the tab runs an opId once and answers a repeat with the first reply, so a retry never makes a clip twice.',
20049
+ inputSchema: {
20050
+ type: "object",
20051
+ properties: { opId: { type: "string", description: "The id from the timeout or disconnect message." } },
20052
+ required: ["opId"]
20053
+ },
20054
+ method: "getOperation"
20055
+ },
20046
20056
  {
20047
20057
  name: "clean_unused_sequences",
20048
20058
  description: "Find nested sequences that no clip uses any more \u2014 left behind when a nest clip is removed or a shot is rebuilt \u2014 and optionally delete them with their bin items. Without apply it only lists them. A scene (a sequence once placed with add_sequence_clip) counts as a nest. Other top-level sequences, and nests reachable from one or open in a tab, are never touched; top-level ones nothing plays are listed under otherUnplayed for delete_sequence.",
@@ -20269,7 +20279,7 @@ var BASE_TOOLS = [
20269
20279
  inputSchema: {
20270
20280
  type: "object",
20271
20281
  properties: {
20272
- url: str("Publicly reachable https URL of the video, audio or image, or a `magnific:<id>` url from search_stock."),
20282
+ url: str("Publicly reachable https URL of the video, audio or image, a `magnific:<id>` url from search_stock, or an `sfx:<id>` url from search_sfx."),
20273
20283
  name: str("Display name for the bin."),
20274
20284
  type: { type: "string", enum: ["video", "audio", "image"], description: "Kind of media." },
20275
20285
  duration: num("Duration in seconds. Required for video and audio, but treated as a hint \u2014 the probe on import overrides it when they disagree."),
@@ -21140,9 +21150,15 @@ var BASE_TOOLS = [
21140
21150
  "cropRight",
21141
21151
  "cropBottom",
21142
21152
  "cropRadius",
21143
- "cropFeather"
21153
+ "cropFeather",
21154
+ "shapeWidth",
21155
+ "shapeHeight",
21156
+ "shapeCornerRadius",
21157
+ "shapeStrokeWidth",
21158
+ "shapeDash",
21159
+ "shapeGap"
21144
21160
  ],
21145
- description: "Property to animate. posX/posY and opacity are 0..1 fractions, rotation is degrees, scaleX/scaleY are multipliers (1 = unchanged), pathTrimEnd 0\u21921 draws a pen stroke on, textPathOffset slides text along its path (0..1) and textPathWave is the height of the wave bending it (px @1080p). cropLeft/Top/Right/Bottom are 0..1 of the layer cut from that side (set_mask crop sets them once), cropRadius rounds the crop's corners and cropFeather softens its edge, both px @1080p."
21161
+ description: "Property to animate. posX/posY and opacity are 0..1 fractions, rotation is degrees, scaleX/scaleY are multipliers (1 = unchanged), pathTrimEnd 0\u21921 draws a pen stroke on, textPathOffset slides text along its path (0..1) and textPathWave is the height of the wave bending it (px @1080p). cropLeft/Top/Right/Bottom are 0..1 of the layer cut from that side (set_mask crop sets them once), cropRadius rounds the crop's corners and cropFeather softens its edge, both px @1080p. shapeWidth/shapeHeight/shapeCornerRadius/shapeStrokeWidth/shapeDash/shapeGap animate a shape layer in its own px @1080p (a square or circle needs width and height keyed together)."
21146
21162
  },
21147
21163
  keyframes: {
21148
21164
  type: "array",
@@ -21168,7 +21184,7 @@ var BASE_TOOLS = [
21168
21184
  clipId: CLIP_ID,
21169
21185
  type: {
21170
21186
  type: "string",
21171
- description: "Effect type: blur, invert, hue, saturation, brightness, contrast, exposure, grayscale, sepia, gradientMap, vignette, filmGrain, flicker, posterizeTime, lensDistortion, chromaticAberration, lensFlare, liquidGlass, opacityFx, colorCorrection, depthMap, gradientMap, vegas, glow, pixelDissolve, motionBlur, dropShadow, reflection, colorama, outline, echo, flutedGlass, lightRays, halftone, datamosh, roughenEdges, filmStock, halation, starGlow, filmDamage, lightLeak, thermal, colorBalance, duotone, levels, monochrome, channelMixer, dither, threshold, fourColorGradient, lensBlur, boxBlur, directionalBlur, spinBlur, zoomBlur, sharpen, cameraShake, bulge, twirl, ripple, waveWarp, polarCoords, turbulentDisplace, elasticGrid, gridSplit, cubify, glitch, vhs, ntsc, crtScreen, ledScreen, rgbSplit, jpegGlitch, ascii, macpaint, modulation, stripe, risograph, emboss, bevel, inkBleed, paperTexture, paperCutout, crossHatch, scatter, blobTracker, edgeGlow, pageTurn, lightSweep, deepGlow, twister, fill, compositeOriginal, grid, repeater, plus transform, layer3d, camera3d (on a camera clip; use set_camera), model3d and showcase. Call describe_effect with a type for its parameters, their ranges and what they do before setting anything but the obvious. An unknown name comes back with the full list."
21187
+ description: "Effect type: blur, invert, hue, saturation, brightness, contrast, exposure, grayscale, sepia, gradientMap, vignette, filmGrain, flicker, posterizeTime, lensDistortion, chromaticAberration, lensFlare, liquidGlass, opacityFx, colorCorrection, depthMap, depthParticles, gradientMap, vegas, glow, pixelDissolve, motionBlur, dropShadow, reflection, colorama, outline, echo, flutedGlass, lightRays, halftone, datamosh, roughenEdges, filmStock, halation, starGlow, filmDamage, lightLeak, thermal, colorBalance, duotone, levels, monochrome, channelMixer, dither, threshold, fourColorGradient, lensBlur, boxBlur, directionalBlur, spinBlur, zoomBlur, sharpen, cameraShake, bulge, twirl, ripple, waveWarp, polarCoords, turbulentDisplace, elasticGrid, gridSplit, cubify, glitch, vhs, ntsc, crtScreen, ledScreen, rgbSplit, jpegGlitch, ascii, macpaint, modulation, stripe, risograph, emboss, bevel, inkBleed, paperTexture, paperCutout, crossHatch, scatter, blobTracker, edgeGlow, pageTurn, lightSweep, deepGlow, twister, fill, compositeOriginal, grid, repeater, plus transform, layer3d, camera3d (on a camera clip; use set_camera), model3d and showcase. Call describe_effect with a type for its parameters, their ranges and what they do before setting anything but the obvious. An unknown name comes back with the full list."
21172
21188
  },
21173
21189
  params: {
21174
21190
  type: "object",
@@ -21358,6 +21374,24 @@ var BASE_TOOLS = [
21358
21374
  },
21359
21375
  method: "searchStock"
21360
21376
  },
21377
+ {
21378
+ name: "search_sfx",
21379
+ description: 'Search GoatEdit\'s sound effect library: originals made by GoatEdit (pops, clicks, whooshes, swishes, risers, thuds, pings, chimes, shine, glitch, typing, page flips, shutter, blips) real CC0 recordings from Freesound (keyboards, paper, whooshes, risers, impacts, shutters, cash register) and Kenney\'s CC0 packs (UI sounds, impacts, power-ups, sci-fi). Every sound is free to use and to export, with no credit owed. Each result carries a `use` line saying which on-screen moment it is for \u2014 pick by that, never by guessing a filename. Bring one in by passing its `url` (sfx:<id>) and `name` to add_media_file (type "audio"), then place it with insert_clip on the sound-effects track (set_audio_master tags a track "sfx"), starting about two frames before the visual it belongs to. It comes in at -16 dB (clip volume 0.16), under the voice; leave that unless it is lost or too loud. Only on visible actions; vary the variant on repeated items.',
21380
+ inputSchema: {
21381
+ type: "object",
21382
+ properties: {
21383
+ query: str('What happens on screen or the sound wanted, e.g. "card lands", "whoosh", "notification".'),
21384
+ category: {
21385
+ type: "string",
21386
+ enum: ["pop", "click", "whoosh", "riser", "impact", "notify", "success", "error", "shine", "glitch", "typing", "paper", "camera", "tech"],
21387
+ description: "Only this kind of sound."
21388
+ },
21389
+ limit: num("How many results. Defaults to 12, at most 40.")
21390
+ },
21391
+ required: ["query"]
21392
+ },
21393
+ method: "searchSfx"
21394
+ },
21361
21395
  {
21362
21396
  name: "create_html_clip",
21363
21397
  description: "Place a CSS-animated HTML snippet on the timeline as a clip. This is the way to get motion that is miserable to keyframe and trivial in CSS: drifting mesh gradients, dot grids, typing input fields, tickers, pulsing badges, UI mock-ups. It renders deterministically \u2014 every animation is paused and seeked per frame \u2014 so the export is frame-identical to the preview, and the clip transforms, fades and keyframes like any other. Prefer CSS or WAAPI animation: a snippet carrying a script or a <video> renders through the costlier sandbox pipeline, and an <iframe> cannot be rasterised at all. Knobs declared as --custom-properties on :host can be overridden per clip through `vars`. Author against the clip box: it is `width` x `height` (defaulting to the project frame), and `100vw`/`100vh` mean exactly that box, not the browser window. Media queries, however, still see the editor window \u2014 size with the units, not with breakpoints. Reference images and footage through `slots` rather than inlining them; the result lists the available slot ids.",
@@ -21391,6 +21425,23 @@ var BASE_TOOLS = [
21391
21425
  },
21392
21426
  method: "addHtmlClip"
21393
21427
  },
21428
+ {
21429
+ name: "update_html_clip",
21430
+ description: "Change an HTML clip made with create_html_clip in place: new markup, some of its --custom-property `vars`, its `slots`, or its render size. The clip keeps its id, position, keyframes, parent and effects \u2014 revise a snippet with this, never by removing and re-creating it. `vars` and `slots` merge over the current values, so pass only what changes. New `html` clears slot overrides (slot ids are element order) unless `slots` is given in the same call. Every clip playing the same snippet changes with it; the reply says how many.",
21431
+ inputSchema: {
21432
+ type: "object",
21433
+ properties: {
21434
+ clipId: str("The HTML clip."),
21435
+ html: str("Replacement markup, with its <style> inline."),
21436
+ vars: { type: "object", description: 'CSS custom properties to change, e.g. { "--accent": "#ff5a1f" }. Merged over the current ones.' },
21437
+ slots: { type: "object", description: "Slot fills to change, keyed by slot id, as in create_html_clip. Merged over the current ones." },
21438
+ width: num("New render width in pixels."),
21439
+ height: num("New render height in pixels.")
21440
+ },
21441
+ required: ["clipId"]
21442
+ },
21443
+ method: "updateHtmlClip"
21444
+ },
21394
21445
  {
21395
21446
  name: "add_transition",
21396
21447
  description: 'Put a transition on a clip. edge "in" (the default) is the clip\'s start: mixed with the clip before it on the same track \u2014 transitions between two shots belong to the incoming clip, so pass the id of the SECOND shot \u2014 or, with nothing before it, transitioned in over whatever is below from its first frame. edge "out" is the clip\'s end: its last `duration` seconds take it out to whatever is below (the entrance played backwards for moves, a fade for dissolves) \u2014 for a clip that ends over a gap, the background or lower layers. A duration that does not fit is refused rather than silently ignored.',
@@ -21891,6 +21942,7 @@ var SEQUENCE_EDITS = /* @__PURE__ */ new Set([
21891
21942
  "setClipEffect",
21892
21943
  "setTextAnimation",
21893
21944
  "addHtmlClip",
21945
+ "updateHtmlClip",
21894
21946
  "setColorGrade",
21895
21947
  "setClipDenoise",
21896
21948
  "addTransition",
@@ -21916,7 +21968,17 @@ var SEQUENCE_EDITS = /* @__PURE__ */ new Set([
21916
21968
  "setMarkers",
21917
21969
  "setScene",
21918
21970
  "setAudioMaster",
21919
- "loopClip"
21971
+ "loopClip",
21972
+ "setCamera",
21973
+ "addShape3D",
21974
+ "updateShape3D",
21975
+ "create3dObject",
21976
+ "createShowcase",
21977
+ "animateClip",
21978
+ "animateScene",
21979
+ "joinScenes",
21980
+ "replaceClipMedia",
21981
+ "retimeRange"
21920
21982
  ]);
21921
21983
  var SEQUENCE_READS = /* @__PURE__ */ new Set([
21922
21984
  "getTimeline",
@@ -21980,6 +22042,39 @@ var TOOLS_BY_NAME = new Map([
21980
22042
  return tool ? [[alias, tool]] : [];
21981
22043
  })
21982
22044
  ]);
22045
+ var READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
22046
+ "get_session_status",
22047
+ "get_design_guide",
22048
+ "get_operation",
22049
+ "get_project_state",
22050
+ "get_transcript",
22051
+ "render_preview_frame",
22052
+ "probe_audio",
22053
+ "export_range",
22054
+ "lint_timeline",
22055
+ "get_edit_log",
22056
+ "list_projects",
22057
+ "list_sequences",
22058
+ "export_design",
22059
+ "get_timeline",
22060
+ "get_clip",
22061
+ "list_checkpoints",
22062
+ "get_media_bin",
22063
+ "describe_motion",
22064
+ "lint_motion",
22065
+ "list_cuts",
22066
+ "measure_text",
22067
+ "describe_effect",
22068
+ "list_presets",
22069
+ "preview_media",
22070
+ "search_stock",
22071
+ "search_sfx",
22072
+ "get_style_profile",
22073
+ "list_plugins",
22074
+ "describe_plugin",
22075
+ "studio_tasks",
22076
+ "studio_workflows"
22077
+ ]);
21983
22078
  function editDistance(a, b) {
21984
22079
  const rows = [Array.from({ length: b.length + 1 }, (_, j) => j)];
21985
22080
  for (let i = 1; i <= a.length; i++) {
@@ -22062,8 +22157,8 @@ var SKILLS = [
22062
22157
  {
22063
22158
  "path": "SKILL.md",
22064
22159
  "uri": "skill://goatedit/goatedit-editing-story/SKILL.md",
22065
- "text": "---\nname: goatedit-editing-story\ndescription: Editing craft for GoatEdit \u2014 story structure and hooks, retention, where and why to cut (emotion, story, rhythm, eye trace), J and L cuts, pacing by format, cutting talking heads, choosing B-roll, captions with exact reading-speed and line rules, music, sound effects, ducking and loudness targets, and colour grading. Use when cutting footage, building a story from a transcript, adding captions, mixing sound, grading, or when an edit drags, confuses, or sounds amateur.\n---\n\n# Editing and story\n\n## Story first\n\nWrite or find the story before touching visuals. Every video, however short,\nhas the same spine:\n\n1. **Hook (0\u20133 s).** Say what this is and why it matters, in words and\n picture, before anything else. Open in the middle of the action, on a\n close-up, on the result, or on a bold claim. No logos, greetings or\n \"in this video\" first.\n2. **Promise.** Tell them what they will get \u2014 the list, the answer, the\n transformation. Previewing a list opens a loop the viewer wants closed.\n3. **Build.** One idea per beat, each raising a question the next answers.\n Cut anything that does not move the story, however good it looks.\n4. **Re-engage.** In anything over about a minute, add a turn, a reveal or a\n change of scene or pace every 45\u201390 s; long videos lose people at the\n points where nothing new arrives.\n5. **Payoff.** Answer the hook. Call back to the opening image.\n6. **Direction.** One clear next step (follow, comment a keyword, visit,\n buy), shown as well as said.\n\n**The test:** watch the first three seconds muted. Would a stranger know what\nthis is and want the rest?\n\n## Where to cut\n\nWhen choosing a cut, weigh in this order (Walter Murch's rule of six):\n**emotion** first, then **story**, then **rhythm**, then **eye trace** (where\nthe viewer is looking at the cut), then the screen's 2D plane, then 3D\ncontinuity. A cut that is right emotionally survives a continuity error; the\nreverse does not.\n\n- **Cut on action.** Cut in the middle of a movement \u2014 a turn, a hand\n reaching, a door \u2014 so the motion hides the cut.\n- **Cut on the eye.** Put the incoming subject where the viewer is already\n looking at the end of the outgoing shot.\n- **J cut:** the next shot's sound starts before its picture (we hear the\n answer, then see the speaker). **L cut:** the current sound carries over the\n next picture (the speaker keeps talking over B-roll). Both make dialogue\n flow; a straight cut on picture and sound together feels abrupt.\n- **Change the shot size or angle** across a cut of the same subject\n (wide to close, or 30\xB0 or more round). A small change looks like a jump.\n- **Keep screen direction.** People and objects keep moving the same way\n across cuts unless something turns them.\n- **Cut music on the beat or the bar**, and music edits on phrase boundaries.\n- **Let it breathe after a big moment** \u2014 hold 0.5\u20131 s before the next idea.\n\n## Pacing by format\n\n| Format | Average shot | Notes |\n| --- | --- | --- |\n| Reel / TikTok / Short | 1.1\u20131.5 s, none over about 3 s | first cut within 1 s, on the first charged word |\n| Ad (6\u201330 s) | 1\u20132 s | product or brand within the first 3\u20135 s |\n| YouTube explainer | 2\u20134 s visuals over steady voice | change the picture at least every 4\u20136 s |\n| Tutorial | as long as the action needs | zoom to the part being discussed; cut dead time |\n| Documentary / brand film | 3\u20136 s, longer holds on faces | let emotional beats breathe |\n| Music video / montage | on the beat; vary 1, 2 and 4 beats | build cut speed toward the drop |\n| Trailer | accelerate across the act; stop dead before the title | |\n\nFaster is not better by itself: change the pace to show what matters. Slow\ndown for the important line, speed up through set-up.\n\n## Cutting talking heads\n\n1. `get_transcript` (or `transcribe`). Every time comes from real word\n timings.\n2. Read every line. Keep the last clean take of each line; cut false starts\n and repeats.\n3. Cut between phrases, never inside one. Trim dead air to under 0.2 s but\n keep natural beats after commas and list markers (0.2\u20130.4 s). Remove a\n filler only when there is silence on both sides. Never speed up a voice.\n4. `apply_ai_edit` with `dryRun: true` first; check each proposed range\n against the transcript before applying.\n5. Hide jump cuts with a punch-in (scale 110\u2013120% on alternate cuts), a\n B-roll cover, or a graphic \u2014 not a dissolve.\n6. Transcripts end words 40\u201380 ms before the sound dies: cut after the decay.\n7. `create_checkpoint` before any destructive pass.\n\n## B-roll\n\n- One visual per noun phrase, starting on its word (at most 0.15 s early).\n- Prefer, in order: real footage of the thing named; a real screen recording\n zoomed to the part discussed; a clearly labelled mock; generated video with\n a concrete metaphor. `search_stock` for footage; check every result with\n `preview_media` before using it.\n- Every still or B-roll shot moves: a slow push of 3\u20138%, or a pan along the\n subject.\n- Use each B-roll shot once. Never invent results or numbers on screen.\n- In vertical video keep the speaker on screen about 45% of the time and\n never gone longer than about 3.5 s; mix split layouts (B-roll above,\n speaker below), full B-roll, and punch-ins.\n\n## Captions\n\n**Readable subtitles** (explainers, interviews, anything people must follow):\n- At most 42 characters per line and two lines, one line when it fits.\n- Reading speed at most 17\u201320 characters a second (about 160\u2013180 words a\n minute); children's content slower.\n- Break lines after punctuation and before conjunctions and prepositions.\n Never split an article from its noun, a first name from a last name, or a\n verb from its pronoun. Bottom-heavy shape.\n- On screen from the first word to just after the last; never across a scene\n cut if it can be avoided \u2014 split it at the cut instead.\n- Numbers one to ten in words, 11 and above in figures.\n\n**Social captions** (reels, shorts, ads):\n- 1\u20133 words per card, split on phrase boundaries (`generate_captions`\n `wordsPerChunk` 2\u20133), hard swaps, on screen two frames before the word.\n- Heavy sans, uppercase, about 60\u201364 px on a 1080\xD71920 frame, in the safe\n area \u2014 never over a mouth.\n- Colour on a quarter to a third of cards, whole card only, with fixed\n meaning (one colour for the key idea, one for the payoff or warning).\n- One style for the whole video; every card in the same box or the same\n treatment. Mixing boxed and bare looks cheap.\n- Captions are an overlay on top of the film, not a reserved band: keep the\n composition centred on the true centre of the frame.\n- Promote a word to a big designed graphic only at a real peak \u2014 at most one\n per beat and never two in a row.\n- Fix names the transcriber cannot know with `glossary`.\n\n## Sound\n\nSound is half the video. You cannot hear it, so measure it (`probe_audio`)\nand ask the user to listen.\n\n- **Tracks:** voice, music and effects on audio tracks of their own \u2014 sound on\n a video track is not mixed. Tag them with `set_audio_master` `roles`.\n- **Voice on top.** Music sits roughly 10\u201314 dB under speech\n (`set_audio_master` `ducking: true`, `duckDb` \u221210 to \u221214) and comes up\n between lines. The first second or two of a voiceover often wants the\n music lower still.\n- **Loudness targets** (integrated, from `probe_audio` `loudness`): YouTube,\n TikTok, Reels and Spotify about \u221214 LUFS; podcasts and Apple about \u221216 LUFS;\n broadcast \u221223 LUFS. True peak at or below \u22121 dBTP \u2014 turn on the limiter\n (`set_audio_master` `limiter: true`, `ceilingDb: -1`).\n- **Music** by mood: confident, darker beats for claims and news; light\n grooves for methods and how-tos; swelling strings or piano for emotion.\n Cut it on bars; end on the music's own ending (or a clean button), never a\n fade in the middle of a phrase. Fade in 0.3\u20131 s; fade out 1\u20133 s.\n- **Sound effects only on visible actions:** a soft air move on a real\n slide, a damped tap when a logo lands, a dry click for UI, paper for\n documents, one warm tone for a success. Vary files on repeated actions.\n No whoosh on every cut; no cartoon zaps unless the look is cartoon.\n- **Room tone.** Never cut voice to digital silence mid-scene; it sounds\n broken.\n- Never fade the last spoken word.\n\n## Grade\n\n- **Fix, then style.** First balance each shot: exposure so faces sit in the\n mids, white balance so whites are white, contrast so blacks are black but\n not crushed (`set_color_grade` exposure, temperature, tint, contrast).\n- **Match shots** in a scene to each other before adding a look; compare\n with `render_preview_frame` side by side.\n- **Skin first.** Midtones (`gamma`) are where skin lives; protect it when\n pushing a look. A look that turns skin green or grey is wrong whatever the\n palette.\n- **One look per video**, chosen in the brief: warm and soft for lifestyle\n and documentary; cool and contrasty for tech and thriller; lifted blacks\n (`lift`) for a film or matte feel; light saturation for premium.\n- Finish with texture when the look wants it: `filmGrain`, `halation`,\n `filmStock`, a gentle `vignette` \u2014 subtle enough that nobody names it.\n- Verify every grade by looking at frames; numbers can look right while the\n picture is wrong.\n",
22066
- "digest": "sha256:f056af026658635792303a5992c77751b19e3e4413f83660ffc7322aeed501b5"
22160
+ "text": '---\nname: goatedit-editing-story\ndescription: Editing craft for GoatEdit \u2014 story structure and hooks, retention, where and why to cut (emotion, story, rhythm, eye trace), J and L cuts, pacing by format, cutting talking heads, choosing B-roll, captions with exact reading-speed and line rules, music, sound effects, ducking and loudness targets, and colour grading. Use when cutting footage, building a story from a transcript, adding captions, mixing sound, grading, or when an edit drags, confuses, or sounds amateur.\n---\n\n# Editing and story\n\n## Story first\n\nWrite or find the story before touching visuals. Every video, however short,\nhas the same spine:\n\n1. **Hook (0\u20133 s).** Say what this is and why it matters, in words and\n picture, before anything else. Open in the middle of the action, on a\n close-up, on the result, or on a bold claim. No logos, greetings or\n "in this video" first.\n2. **Promise.** Tell them what they will get \u2014 the list, the answer, the\n transformation. Previewing a list opens a loop the viewer wants closed.\n3. **Build.** One idea per beat, each raising a question the next answers.\n Cut anything that does not move the story, however good it looks.\n4. **Re-engage.** In anything over about a minute, add a turn, a reveal or a\n change of scene or pace every 45\u201390 s; long videos lose people at the\n points where nothing new arrives.\n5. **Payoff.** Answer the hook. Call back to the opening image.\n6. **Direction.** One clear next step (follow, comment a keyword, visit,\n buy), shown as well as said.\n\n**The test:** watch the first three seconds muted. Would a stranger know what\nthis is and want the rest?\n\n## Where to cut\n\nWhen choosing a cut, weigh in this order (Walter Murch\'s rule of six):\n**emotion** first, then **story**, then **rhythm**, then **eye trace** (where\nthe viewer is looking at the cut), then the screen\'s 2D plane, then 3D\ncontinuity. A cut that is right emotionally survives a continuity error; the\nreverse does not.\n\n- **Cut on action.** Cut in the middle of a movement \u2014 a turn, a hand\n reaching, a door \u2014 so the motion hides the cut.\n- **Cut on the eye.** Put the incoming subject where the viewer is already\n looking at the end of the outgoing shot.\n- **J cut:** the next shot\'s sound starts before its picture (we hear the\n answer, then see the speaker). **L cut:** the current sound carries over the\n next picture (the speaker keeps talking over B-roll). Both make dialogue\n flow; a straight cut on picture and sound together feels abrupt.\n- **Change the shot size or angle** across a cut of the same subject\n (wide to close, or 30\xB0 or more round). A small change looks like a jump.\n- **Keep screen direction.** People and objects keep moving the same way\n across cuts unless something turns them.\n- **Cut music on the beat or the bar**, and music edits on phrase boundaries.\n- **Let it breathe after a big moment** \u2014 hold 0.5\u20131 s before the next idea.\n\n## Pacing by format\n\n| Format | Average shot | Notes |\n| --- | --- | --- |\n| Reel / TikTok / Short | 1.1\u20131.5 s, none over about 3 s | first cut within 1 s, on the first charged word |\n| Ad (6\u201330 s) | 1\u20132 s | product or brand within the first 3\u20135 s |\n| YouTube explainer | 2\u20134 s visuals over steady voice | change the picture at least every 4\u20136 s |\n| Tutorial | as long as the action needs | zoom to the part being discussed; cut dead time |\n| Documentary / brand film | 3\u20136 s, longer holds on faces | let emotional beats breathe |\n| Music video / montage | on the beat; vary 1, 2 and 4 beats | build cut speed toward the drop |\n| Trailer | accelerate across the act; stop dead before the title | |\n\nFaster is not better by itself: change the pace to show what matters. Slow\ndown for the important line, speed up through set-up.\n\n## Cutting talking heads\n\n1. `get_transcript` (or `transcribe`). Every time comes from real word\n timings.\n2. Read every line. Keep the last clean take of each line; cut false starts\n and repeats.\n3. Cut between phrases, never inside one. Trim dead air to under 0.2 s but\n keep natural beats after commas and list markers (0.2\u20130.4 s). Remove a\n filler only when there is silence on both sides. Never speed up a voice.\n4. `apply_ai_edit` with `dryRun: true` first; check each proposed range\n against the transcript before applying.\n5. Hide jump cuts with a punch-in (scale 110\u2013120% on alternate cuts), a\n B-roll cover, or a graphic \u2014 not a dissolve.\n6. Transcripts end words 40\u201380 ms before the sound dies: cut after the decay.\n7. `create_checkpoint` before any destructive pass.\n\n## B-roll\n\n- One visual per noun phrase, starting on its word (at most 0.15 s early).\n- Prefer, in order: real footage of the thing named; a real screen recording\n zoomed to the part discussed; a clearly labelled mock; generated video with\n a concrete metaphor. `search_stock` for footage; check every result with\n `preview_media` before using it.\n- Every still or B-roll shot moves: a slow push of 3\u20138%, or a pan along the\n subject.\n- Use each B-roll shot once. Never invent results or numbers on screen.\n- In vertical video keep the speaker on screen about 45% of the time and\n never gone longer than about 3.5 s; mix split layouts (B-roll above,\n speaker below), full B-roll, and punch-ins.\n\n## Captions\n\n**Readable subtitles** (explainers, interviews, anything people must follow):\n- At most 42 characters per line and two lines, one line when it fits.\n- Reading speed at most 17\u201320 characters a second (about 160\u2013180 words a\n minute); children\'s content slower.\n- Break lines after punctuation and before conjunctions and prepositions.\n Never split an article from its noun, a first name from a last name, or a\n verb from its pronoun. Bottom-heavy shape.\n- On screen from the first word to just after the last; never across a scene\n cut if it can be avoided \u2014 split it at the cut instead.\n- Numbers one to ten in words, 11 and above in figures.\n\n**Social captions** (reels, shorts, ads):\n- 1\u20133 words per card, split on phrase boundaries (`generate_captions`\n `wordsPerChunk` 2\u20133), hard swaps, on screen two frames before the word.\n- Heavy sans, uppercase, about 60\u201364 px on a 1080\xD71920 frame, in the safe\n area \u2014 never over a mouth.\n- Colour on a quarter to a third of cards, whole card only, with fixed\n meaning (one colour for the key idea, one for the payoff or warning).\n- One style for the whole video; every card in the same box or the same\n treatment. Mixing boxed and bare looks cheap.\n- Captions are an overlay on top of the film, not a reserved band: keep the\n composition centred on the true centre of the frame.\n- Promote a word to a big designed graphic only at a real peak \u2014 at most one\n per beat and never two in a row.\n- Fix names the transcriber cannot know with `glossary`.\n\n## Sound\n\nSound is half the video. You cannot hear it, so measure it (`probe_audio`)\nand ask the user to listen.\n\n- **Tracks:** voice, music and effects on audio tracks of their own \u2014 sound on\n a video track is not mixed. Tag them with `set_audio_master` `roles`.\n- **Voice on top.** Music sits roughly 10\u201314 dB under speech\n (`set_audio_master` `ducking: true`, `duckDb` \u221210 to \u221214) and comes up\n between lines. The first second or two of a voiceover often wants the\n music lower still.\n- **Loudness targets** (integrated, from `probe_audio` `loudness`): YouTube,\n TikTok, Reels and Spotify about \u221214 LUFS; podcasts and Apple about \u221216 LUFS;\n broadcast \u221223 LUFS. True peak at or below \u22121 dBTP \u2014 turn on the limiter\n (`set_audio_master` `limiter: true`, `ceilingDb: -1`).\n- **Music** by mood: confident, darker beats for claims and news; light\n grooves for methods and how-tos; swelling strings or piano for emotion.\n Cut it on bars; end on the music\'s own ending (or a clean button), never a\n fade in the middle of a phrase. Fade in 0.3\u20131 s; fade out 1\u20133 s.\n- **Sound effects only on visible actions:** a soft air move on a real\n slide, a damped tap when a logo lands, a dry click for UI, paper for\n documents, one warm tone for a success. Vary files on repeated actions.\n No whoosh on every cut; no cartoon zaps unless the look is cartoon.\n Find them with `search_sfx` by the on-screen action ("card lands",\n "slides in", "typing") and pick by each result\'s `use` line; add with\n `add_media_file` (`sfx:<id>`), place on the sfx-role track about two frames\n before the visual. They come in at -16 dB; change that only by ear.\n- **Room tone.** Never cut voice to digital silence mid-scene; it sounds\n broken.\n- Never fade the last spoken word.\n\n## Grade\n\n- **Fix, then style.** First balance each shot: exposure so faces sit in the\n mids, white balance so whites are white, contrast so blacks are black but\n not crushed (`set_color_grade` exposure, temperature, tint, contrast).\n- **Match shots** in a scene to each other before adding a look; compare\n with `render_preview_frame` side by side.\n- **Skin first.** Midtones (`gamma`) are where skin lives; protect it when\n pushing a look. A look that turns skin green or grey is wrong whatever the\n palette.\n- **One look per video**, chosen in the brief: warm and soft for lifestyle\n and documentary; cool and contrasty for tech and thriller; lifted blacks\n (`lift`) for a film or matte feel; light saturation for premium.\n- Finish with texture when the look wants it: `filmGrain`, `halation`,\n `filmStock`, a gentle `vignette` \u2014 subtle enough that nobody names it.\n- Verify every grade by looking at frames; numbers can look right while the\n picture is wrong.\n',
22161
+ "digest": "sha256:2409fecd10f0230711b51f5e810245feb29df80c2f9555f400e5911a61cefab6"
22067
22162
  }
22068
22163
  ]
22069
22164
  },
@@ -22096,8 +22191,8 @@ var SKILLS = [
22096
22191
  {
22097
22192
  "path": "SKILL.md",
22098
22193
  "uri": "skill://goatedit/goatedit-motion-design/SKILL.md",
22099
- "text": '---\nname: goatedit-motion-design\ndescription: Motion design for GoatEdit \u2014 easing, durations, stagger, overshoot, hierarchy and holds with exact numbers; how one scene hands motion to the next (direction, matched speed, cutting mid-motion, carriers); the seam techniques (push, zoom-through, inverse zoom, match cut, morph, rack focus); keeping a scene alive without idle wobble; kinetic typography; and which GoatEdit tool does each. Use before animating anything, joining scenes, or when a video feels static, jumpy, floaty or "like slides".\n---\n\n# Motion design\n\nMotion has one job: show the viewer where to look next. Every move answers\n"what changed, and where should the eye go?" A video should feel like one\ncontinuous camera move through ideas, not a stack of slides that each animate\non their own.\n\n## The numbers\n\n| Principle | Rule |\n| --- | --- |\n| Easing | Never linear. Arriving: ease-out `[0.22,1,0.36,1]`. Leaving: ease-in `[0.55,0,1,0.45]`. Moving on screen: in-out `[0.65,0,0.35,1]`. Snappy arrivals: expo-out. |\n| Duration | Nudge 0.15 s. Entrance 0.3\u20130.5 s, never one element over 0.8 s. Across the frame 0.4\u20130.7 s. Camera push at a cut 0.35\u20130.45 s each side. Exit about 75% of its entrance. |\n| Stagger | 30\u201360 ms between siblings, in reading order or outward from the hero; the whole group inside 0.4\u20130.5 s. With 8+ items tighten the gap rather than lengthen the group. |\n| Overshoot | Pops and arrivals overshoot 5\u201310% and settle. Never on camera moves. No bounce-out or elastic-out eases: they read as a toy. |\n| Anticipation | A big exit starts with 0.1 s the other way, 3\u20135% of the distance. |\n| Hierarchy | One hero move at a time; everything else smaller, shorter or later. |\n| Consistency | Similar elements share one ease and one duration. Never a unique pair per element. |\n| Arcs | Organic things travel on curves; type and UI may go straight. |\n| Squash and stretch | Fast round things stretch along their travel and squash on impact. |\n| Motion blur | Only on fast moves, 2D layers only. |\n| Parallax | Background layers move 30\u201360% as far as the foreground. |\n| Holds | After an impact or reveal, hold 0.3\u20130.6 s. Text stays 0.3 s a word plus 0.5 s. |\n| Stillness before the payoff | Leave 0.3\u20130.75 s between the action and its result \u2014 the dramatic comma. |\n| Rhythm | Alternate fast and slow. Don\'t use the same join twice running unless it is a pattern. |\n| Sync | A visual lands 0.1\u20130.25 s before the word it shows, never after. With music, hero moves land on beats. |\n\n## One film, one current\n\n**Pick one direction for the whole video** \u2014 say, leftward \u2014 and send every\nordinary scene change that way. Other directions are reserved and mean\nsomething when spent:\n\n| Vector | Means |\n| --- | --- |\n| The current (e.g. left) | next beat, forward progress |\n| Up | a conclusion or reveal rising above what came before |\n| Zoom forward (push in) | going deeper into the same thought |\n| Zoom backward (pull out) | arrival: something bigger lands, the payoff |\n\n- Never run two scene changes back to back in opposite directions; ping-pong\n reads as a mistake. A change of direction needs a visible cause (a click,\n an impact) or a new chapter.\n\n## How one scene hands to the next\n\n**The rule:** how scene A leaves decides how scene B arrives \u2014 same axis,\nsame direction, matched speed, and the cut falls mid-motion on both sides.\n\n1. **Axis.** Sideways stays sideways, vertical stays vertical, zoom stays\n zoom.\n2. **Direction.** Never mirror. On zoom, direction is whether things grow or\n shrink: a shrinking (pull-back) exit must be answered by an entrance that\n also shrinks into place (starts oversized), not one that grows from small.\n This is the most common mistake, because grow-from-small is everyone\'s\n default entrance.\n3. **Speed.** Exit with ease-in, enter with the matching ease-out, over the\n same distance and time \u2014 the two halves of one in-out curve, so the speed\n matches at the cut.\n4. **Phase.** Cut while both sides are moving. A scene that settles to rest\n before the cut, or an entrance that starts from rest after it, is a dead\n beat.\n5. **No crossfades between scenes.** A dissolve has nothing for the eye to\n follow. Keep dissolves for time passing or a dream-like mood.\n\n**Carriers.** The eye follows objects. The strongest join hands a concrete\nthing across the cut at the same place and speed: a card that becomes the\nnext screen, a word that becomes a heading, a dot that becomes a map pin, a\ncursor mid-path. Plan carriers at storyboard time.\n\n**Partial travel.** Scene pushes travel about 12% of the frame (about 230 px\nat 1920) with a quick fade, not a full-frame slide. Full-frame slides look\nlike a slideshow.\n\n## The joins, and the GoatEdit tool for each\n\n`list_cuts` shows every cut and which ones are still (`stillCuts`).\n`join_scenes` makes them flow. `auto` picks one and says why in `picked`.\n\n| Technique | Use for | GoatEdit |\n| --- | --- | --- |\n| **Push** (cut the curve) | the default scene change, in the film\'s current | `join_scenes` type `push`, `direction` = the current, `duration` 0.3\u20130.4 |\n| **Zoom-through** | going deeper: fly into one hero element, the next scene opens out of it | `join_scenes` type `zoomThrough`, `clipId` = that element. Never into one letter of a row |\n| **Inverse zoom** | the payoff arriving: outgoing shrinks away, incoming arrives oversized and settles | push-out camera keys with `set_keyframes`: out 1\u21920.8, in 1.25\u21921, expo-out, about 0.7 s total. Spend it once or twice per video |\n| **Match cut** | one layer becomes another in the same spot | `join_scenes` type `matchCut` with `fromClipId`/`toClipId` |\n| **Morph** | every layer grows out of its partner (Smart Animate) \u2014 the strongest join | `join_scenes` type `morph`; name partners with the same last word |\n| **Rack focus** | the one cut you want seen: same surface, new state | blur spike 8\u201312 px peak at the cut with a 1.06 scale breath (`set_clip_effect` blur keyframes); at most once every 8 s |\n| **Hard cut on a beat** | energy, music videos, reels | no join; cut exactly on the beat marker |\n\nBlur on zoom joins: about 10 px for text-sized subjects, 18\u201320 px for\nfull-frame surfaces. Use two or three join types per video and repeat them;\nvariety for its own sake reads as a template showcase.\n\n## Keeping a scene alive\n\nA scene that finishes its entrance with seconds left is a planning problem.\nFill it with story, not wobble. Idle loops on graphics (floating, breathing,\npulsing glow) read as "the video is waiting" \u2014 don\'t. Each stretch between\nentrance and exit belongs to one of these:\n\n- **Staged reveals** \u2014 hold content back and reveal it on the words that\n name it. The default when a scene has two or more ideas.\n- **Camera with intent** \u2014 start wide, travel, arrive on the subject. A slow\n push on a photo or footage (3\u20138% over the shot) counts; `animate_scene`\'s\n `hold` drift on pictures does this.\n A push through a scene with depth is not one zoom: split it into planes\n (backdrop, subject, props in front) and let each grow at its own rate \u2014\n near things grow faster than far ones. Everything on one null scaled\n equally reads flat. The direct way: switch each plane to 3D\n (`set_clip_effect` type `layer3d`, `posZ` 0 for the subject, 800\u20132000 for\n backdrops, negative for foreground props) and move one `set_camera`: a\n dolly (`z`), truck (`x`), orbit (`orbit` with `pivotZ` at the subject\'s\n depth) or lens pull (`focalLength`) moves every plane by its own depth.\n 3D planes stack by depth, not track, so an orbit passes them correctly;\n keep 2D layers (titles, adjustment layers) above or below all of them\n (`lint_timeline` flags one that splits them). For a backdrop set layer3d\n `fill: 1`: it grows just enough to cover the frame for the camera\'s whole\n move, so an orbit never shows its edge. `aperture` + `focus` racks focus between planes.\n- **Rig, don\'t copy keys** \u2014 `set_keyframes` `expression` links properties\n the way After Effects does: a null named "Controls" whose `posX` drives\n many layers (`layer("Controls").posX * 100`), a cascade with\n `valueAtTime(time - (index - 1) * 0.08)` on shared keys, an idle bob as two\n keys plus `loopOut("pingpong")`, `thisLayer.scaleX` to keep proportions.\n Change the one control, and everything tied to it follows.\n- **The product doing its thing** \u2014 a progress bar fills, a count ticks, a\n highlight steps down a list.\n- **Acted-out beats** \u2014 a card files into a stack, an item is dragged, a\n result assembles.\n- **Cursor-led** \u2014 an oversized cursor walks to a button and its click\n triggers the next beat.\n\nTest: pause on any second. Something meaningful should be mid-flight.\n\n**Cause and effect.** Chain moves so each is launched by the last \u2014 click,\nsquash, release, flight, impact, recoil, reveal. A reaction starts on the\nframe of its cause, not "shortly after". Heavy things rebound slower; small\nthings snap.\n\n## Entrances\n\n- **Waterfall entry** for titles, openers and lists: words arrive from below\n one after another, each starting before the last settles, gaps shrinking,\n the last one snapping. Heavy words travel 60\u201380 px over 0.16\u20130.2 s, normal\n words 40\u201350 px over 0.13\u20130.16 s, small words 30\u201345 px over 0.1\u20130.13 s.\n Opacity cuts on rather than fading.\n- **One direction per cascade.** Never ease-in-out on an entrance.\n- **Text keeps its own animation.** A text layer gets a text preset\n (`set_text_animation`, letters or words, `delay` for its place in the\n cascade), never a block keyframe move.\n- **Text leaves the way it came.** `set_text_animation` with\n `exit: { preset, duration, stagger, order }` plays a preset out at the clip\n end, carrying on in its own direction (rises in, rises out). Exit alone,\n with no entrance, keeps the words on from frame one. For a typed line that\n deletes itself, `unit: "letter"` plus `exit: { preset: "typewriterCursor",\n stagger: 0.03 }`: last letter first, with a caret that blinks just before.\n Check the reply\'s `exitStartsAt` and any overlap warning; `exit: null`\n removes it. Kinetic presets and countUp cannot be exits.\n\n| Feel | Hero title | Text | Row of words | Picture hero | Support |\n| --- | --- | --- | --- | --- | --- |\n| smooth | `cinematicRise` | `riseFade` | `riseFade` | blurIn, zoomIn, maskReveal | fadeUp |\n| snappy | `maskRise` | `slideUp` | `maskRise` | popIn, slideIn, maskReveal | slideIn outward from the hero |\n| bouncy | `springUp` | `scaleIn` | `bounceDrop` | dropIn, popIn, spinIn | popIn |\n\nPick one feel for the whole video. `animate_scene` applies these by role\n(ground, hero, support, text, detail, follower) with `style` = the feel; use\n`dryRun: true` to see the plan, then hand-key only the hero move.\n\n## Kinetic typography\n\n- Time every word to the voice: `set_text_animation` with `syncToWords`, or\n cut each word on at its real start from `get_transcript`.\n- One to four words on screen at a time; the key word bigger, heavier or in\n the accent.\n- Vary scale and position with the rhythm of the line \u2014 a stressed word hits\n harder (`slamIn`, `focusPull`), a soft word drifts (`riseFade`).\n- Move the camera, not every word: a group of words can push or rotate as one\n (parent them to a null with `add_null` and `set_parent`).\n- Hand from line to line with a word-level push in the current\'s direction.\n\n## Stepped motion\n\nFor a hand-made, editorial or collage feel, step graphics at 12 fps inside a\n24\u201330 fps video (`set_clip_effect` `posterizeTime`). Keep camera moves and\nfootage smooth.\n\n## Workflow in GoatEdit\n\n1. Approved stills, one element per layer, layers named for their partners.\n2. `animate_scene` (`all: true` or per scene) in one feel.\n3. Hand-key the hero move with `set_keyframes` (times relative to the clip\n start; posX/posY are fractions of the frame).\n4. `list_cuts`, then `join_scenes` on each still cut, in the current.\n5. `lint_motion`; run each finding\'s `fix` calls one at a time; lint again\n until only judgement notes remain.\n6. `export_range` strips around every join and hero move, then fix what you\n see. A lint pass is not a watch.\n\nGoatEdit specifics: HTML clips crop at their box when moved \u2014 animate inside\nthem with CSS. 3D frames render slowly through the bridge, so keep strips\nshort. `describe_motion` returns the numbers `lint_motion` checks.\n',
22100
- "digest": "sha256:22953267f71aa7809e8700016d831ae2db3fb9ffd76e27c19d2c0a3b4429b24f"
22194
+ "text": '---\nname: goatedit-motion-design\ndescription: Motion design for GoatEdit \u2014 easing, durations, stagger, overshoot, hierarchy and holds with exact numbers; how one scene hands motion to the next (direction, matched speed, cutting mid-motion, carriers); the seam techniques (push, zoom-through, inverse zoom, match cut, morph, rack focus); keeping a scene alive without idle wobble; kinetic typography; and which GoatEdit tool does each. Use before animating anything, joining scenes, or when a video feels static, jumpy, floaty or "like slides".\n---\n\n# Motion design\n\nMotion has one job: show the viewer where to look next. Every move answers\n"what changed, and where should the eye go?" A video should feel like one\ncontinuous camera move through ideas, not a stack of slides that each animate\non their own.\n\n## The numbers\n\n| Principle | Rule |\n| --- | --- |\n| Easing | Never linear. Arriving: ease-out `[0.22,1,0.36,1]`. Leaving: ease-in `[0.55,0,1,0.45]`. Moving on screen: in-out `[0.65,0,0.35,1]`. Snappy arrivals: expo-out. |\n| Duration | Nudge 0.15 s. Entrance 0.3\u20130.5 s, never one element over 0.8 s. Across the frame 0.4\u20130.7 s. Camera push at a cut 0.35\u20130.45 s each side. Exit about 75% of its entrance. |\n| Stagger | 30\u201360 ms between siblings, in reading order or outward from the hero; the whole group inside 0.4\u20130.5 s. With 8+ items tighten the gap rather than lengthen the group. |\n| Overshoot | Pops and arrivals overshoot 5\u201310% and settle. Never on camera moves. No bounce-out or elastic-out eases: they read as a toy. |\n| Anticipation | A big exit starts with 0.1 s the other way, 3\u20135% of the distance. |\n| Hierarchy | One hero move at a time; everything else smaller, shorter or later. |\n| Consistency | Similar elements share one ease and one duration. Never a unique pair per element. |\n| Arcs | Organic things travel on curves; type and UI may go straight. |\n| Squash and stretch | Fast round things stretch along their travel and squash on impact. |\n| Motion blur | Only on fast moves, 2D layers only. |\n| Parallax | Background layers move 30\u201360% as far as the foreground. |\n| Holds | After an impact or reveal, hold 0.3\u20130.6 s. Text stays 0.3 s a word plus 0.5 s. |\n| Stillness before the payoff | Leave 0.3\u20130.75 s between the action and its result \u2014 the dramatic comma. |\n| Rhythm | Alternate fast and slow. Don\'t use the same join twice running unless it is a pattern. |\n| Sync | A visual lands 0.1\u20130.25 s before the word it shows, never after. With music, hero moves land on beats. |\n\n## One film, one current\n\n**Pick one direction for the whole video** \u2014 say, leftward \u2014 and send every\nordinary scene change that way. Other directions are reserved and mean\nsomething when spent:\n\n| Vector | Means |\n| --- | --- |\n| The current (e.g. left) | next beat, forward progress |\n| Up | a conclusion or reveal rising above what came before |\n| Zoom forward (push in) | going deeper into the same thought |\n| Zoom backward (pull out) | arrival: something bigger lands, the payoff |\n\n- Never run two scene changes back to back in opposite directions; ping-pong\n reads as a mistake. A change of direction needs a visible cause (a click,\n an impact) or a new chapter.\n\n## How one scene hands to the next\n\n**The rule:** how scene A leaves decides how scene B arrives \u2014 same axis,\nsame direction, matched speed, and the cut falls mid-motion on both sides.\n\n1. **Axis.** Sideways stays sideways, vertical stays vertical, zoom stays\n zoom.\n2. **Direction.** Never mirror. On zoom, direction is whether things grow or\n shrink: a shrinking (pull-back) exit must be answered by an entrance that\n also shrinks into place (starts oversized), not one that grows from small.\n This is the most common mistake, because grow-from-small is everyone\'s\n default entrance.\n3. **Speed.** Exit with ease-in, enter with the matching ease-out, over the\n same distance and time \u2014 the two halves of one in-out curve, so the speed\n matches at the cut.\n4. **Phase.** Cut while both sides are moving. A scene that settles to rest\n before the cut, or an entrance that starts from rest after it, is a dead\n beat.\n5. **No crossfades between scenes.** A dissolve has nothing for the eye to\n follow. Keep dissolves for time passing or a dream-like mood.\n\n**Carriers.** The eye follows objects. The strongest join hands a concrete\nthing across the cut at the same place and speed: a card that becomes the\nnext screen, a word that becomes a heading, a dot that becomes a map pin, a\ncursor mid-path. Plan carriers at storyboard time.\n\n**Partial travel.** Scene pushes travel about 12% of the frame (about 230 px\nat 1920) with a quick fade, not a full-frame slide. Full-frame slides look\nlike a slideshow.\n\n## The joins, and the GoatEdit tool for each\n\n`list_cuts` shows every cut and which ones are still (`stillCuts`).\n`join_scenes` makes them flow. `auto` picks one and says why in `picked`.\n\n| Technique | Use for | GoatEdit |\n| --- | --- | --- |\n| **Push** (cut the curve) | the default scene change, in the film\'s current | `join_scenes` type `push`, `direction` = the current, `duration` 0.3\u20130.4 |\n| **Zoom-through** | going deeper: fly into one hero element, the next scene opens out of it | `join_scenes` type `zoomThrough`, `clipId` = that element. Never into one letter of a row |\n| **Inverse zoom** | the payoff arriving: outgoing shrinks away, incoming arrives oversized and settles | push-out camera keys with `set_keyframes`: out 1\u21920.8, in 1.25\u21921, expo-out, about 0.7 s total. Spend it once or twice per video |\n| **Match cut** | one layer becomes another in the same spot | `join_scenes` type `matchCut` with `fromClipId`/`toClipId` |\n| **Morph** | every layer grows out of its partner (Smart Animate) \u2014 the strongest join | `join_scenes` type `morph`; name partners with the same last word |\n| **Rack focus** | the one cut you want seen: same surface, new state | blur spike 8\u201312 px peak at the cut with a 1.06 scale breath (`set_clip_effect` blur keyframes); at most once every 8 s |\n| **Hard cut on a beat** | energy, music videos, reels | no join; cut exactly on the beat marker |\n\nBlur on zoom joins: about 10 px for text-sized subjects, 18\u201320 px for\nfull-frame surfaces. Use two or three join types per video and repeat them;\nvariety for its own sake reads as a template showcase.\n\n## Keeping a scene alive\n\nA scene that finishes its entrance with seconds left is a planning problem.\nFill it with story, not wobble. Idle loops on graphics (floating, breathing,\npulsing glow) read as "the video is waiting" \u2014 don\'t. Each stretch between\nentrance and exit belongs to one of these:\n\n- **Staged reveals** \u2014 hold content back and reveal it on the words that\n name it. The default when a scene has two or more ideas.\n- **Camera with intent** \u2014 start wide, travel, arrive on the subject. A slow\n push on a photo or footage (3\u20138% over the shot) counts; `animate_scene`\'s\n `hold` drift on pictures does this.\n A push through a scene with depth is not one zoom: split it into planes\n (backdrop, subject, props in front) and let each grow at its own rate \u2014\n near things grow faster than far ones. Everything on one null scaled\n equally reads flat. The direct way: switch each plane to 3D\n (`set_clip_effect` type `layer3d`, `posZ` 0 for the subject, 800\u20132000 for\n backdrops, negative for foreground props) and move one `set_camera`: a\n dolly (`z`), truck (`x`), orbit (`orbit` with `pivotZ` at the subject\'s\n depth) or lens pull (`focalLength`) moves every plane by its own depth.\n 3D planes stack by depth, not track, so an orbit passes them correctly;\n keep 2D layers (titles, adjustment layers) above or below all of them\n (`lint_timeline` flags one that splits them). For a backdrop set layer3d\n `fill: 1`: it grows just enough to cover the frame for the camera\'s whole\n move, so an orbit never shows its edge. `aperture` + `focus` racks focus between planes.\n- **Rig, don\'t copy keys** \u2014 `set_keyframes` `expression` links properties\n the way After Effects does: a null named "Controls" whose `posX` drives\n many layers (`layer("Controls").posX * 100`), a cascade with\n `valueAtTime(time - (index - 1) * 0.08)` on shared keys, an idle bob as two\n keys plus `loopOut("pingpong")`, `thisLayer.scaleX` to keep proportions.\n Change the one control, and everything tied to it follows.\n- **Animate the shape, not just its transform** \u2014 `set_keyframes` keys a\n shape layer\'s `shapeWidth`, `shapeHeight`, `shapeCornerRadius`,\n `shapeStrokeWidth`, `shapeDash` and `shapeGap`: a bar that grows from its\n edge instead of stretching its corners, a pill that rounds into a circle.\n Key width and height together on a square or circle.\n- **The product doing its thing** \u2014 a progress bar fills, a count ticks, a\n highlight steps down a list.\n- **Acted-out beats** \u2014 a card files into a stack, an item is dragged, a\n result assembles.\n- **Cursor-led** \u2014 an oversized cursor walks to a button and its click\n triggers the next beat.\n\nTest: pause on any second. Something meaningful should be mid-flight.\n\n**Cause and effect.** Chain moves so each is launched by the last \u2014 click,\nsquash, release, flight, impact, recoil, reveal. A reaction starts on the\nframe of its cause, not "shortly after". Heavy things rebound slower; small\nthings snap.\n\n## Entrances\n\n- **Waterfall entry** for titles, openers and lists: words arrive from below\n one after another, each starting before the last settles, gaps shrinking,\n the last one snapping. Heavy words travel 60\u201380 px over 0.16\u20130.2 s, normal\n words 40\u201350 px over 0.13\u20130.16 s, small words 30\u201345 px over 0.1\u20130.13 s.\n Opacity cuts on rather than fading.\n- **One direction per cascade.** Never ease-in-out on an entrance.\n- **Text keeps its own animation.** A text layer gets a text preset\n (`set_text_animation`, letters or words, `delay` for its place in the\n cascade), never a block keyframe move.\n- **Text leaves the way it came.** `set_text_animation` with\n `exit: { preset, duration, stagger, order }` plays a preset out at the clip\n end, carrying on in its own direction (rises in, rises out). Exit alone,\n with no entrance, keeps the words on from frame one. For a typed line that\n deletes itself, `unit: "letter"` plus `exit: { preset: "typewriterCursor",\n stagger: 0.03 }`: last letter first, with a caret that blinks just before.\n Check the reply\'s `exitStartsAt` and any overlap warning; `exit: null`\n removes it. Kinetic presets and countUp cannot be exits.\n\n| Feel | Hero title | Text | Row of words | Picture hero | Support |\n| --- | --- | --- | --- | --- | --- |\n| smooth | `cinematicRise` | `riseFade` | `riseFade` | blurIn, zoomIn, maskReveal | fadeUp |\n| snappy | `maskRise` | `slideUp` | `maskRise` | popIn, slideIn, maskReveal | slideIn outward from the hero |\n| bouncy | `springUp` | `scaleIn` | `bounceDrop` | dropIn, popIn, spinIn | popIn |\n\nPick one feel for the whole video. `animate_scene` applies these by role\n(ground, hero, support, text, detail, follower) with `style` = the feel; use\n`dryRun: true` to see the plan, then hand-key only the hero move.\n\n## Kinetic typography\n\n- Time every word to the voice: `set_text_animation` with `syncToWords`, or\n cut each word on at its real start from `get_transcript`.\n- One to four words on screen at a time; the key word bigger, heavier or in\n the accent.\n- Vary scale and position with the rhythm of the line \u2014 a stressed word hits\n harder (`slamIn`, `focusPull`), a soft word drifts (`riseFade`).\n- Move the camera, not every word: a group of words can push or rotate as one\n (parent them to a null with `add_null` and `set_parent`).\n- Hand from line to line with a word-level push in the current\'s direction.\n\n## Stepped motion\n\nFor a hand-made, editorial or collage feel, step graphics at 12 fps inside a\n24\u201330 fps video (`set_clip_effect` `posterizeTime`). Keep camera moves and\nfootage smooth.\n\n## Workflow in GoatEdit\n\n1. Approved stills, one element per layer, layers named for their partners.\n2. `animate_scene` (`all: true` or per scene) in one feel.\n3. Hand-key the hero move with `set_keyframes` (times relative to the clip\n start; posX/posY are fractions of the frame).\n4. `list_cuts`, then `join_scenes` on each still cut, in the current.\n5. `lint_motion`; run each finding\'s `fix` calls one at a time; lint again\n until only judgement notes remain.\n6. `export_range` strips around every join and hero move, then fix what you\n see. A lint pass is not a watch.\n\nGoatEdit specifics: HTML clips crop at their box when moved \u2014 animate inside\nthem with CSS. 3D frames render slowly through the bridge, so keep strips\nshort. `describe_motion` returns the numbers `lint_motion` checks.\n',
22195
+ "digest": "sha256:b3e82f40c23ddc9983d492580a4bd7c4647e2415d96d6245b839fe2253032101"
22101
22196
  }
22102
22197
  ]
22103
22198
  },
@@ -22339,6 +22434,10 @@ function withRunSequence(stepArgs, sequenceId, tool) {
22339
22434
  if (!tool || !("expectedSequenceId" in (tool.inputSchema.properties ?? {}))) return stepArgs;
22340
22435
  return { ...stepArgs, expectedSequenceId: sequenceId };
22341
22436
  }
22437
+ function withRunOp(stepArgs, runOpId, index2) {
22438
+ if (!runOpId || typeof stepArgs.opId === "string") return stepArgs;
22439
+ return { ...stepArgs, opId: `${runOpId}.${index2 + 1}` };
22440
+ }
22342
22441
 
22343
22442
  // src/tab-registry.ts
22344
22443
  function indexTabs(reports, isOpen) {
@@ -25166,13 +25265,16 @@ function resolveTab(projectId) {
25166
25265
  if (!id) throw new Error(notConnectedMessage());
25167
25266
  return tabs.get(id);
25168
25267
  }
25268
+ function unsettled(message) {
25269
+ return Object.assign(new Error(message), { unsettled: true });
25270
+ }
25169
25271
  var pendingRequests = /* @__PURE__ */ new Map();
25170
25272
  function rejectPendingOn(socket, why) {
25171
25273
  for (const [id, pending] of pendingRequests) {
25172
25274
  if (pending.socket !== socket) continue;
25173
25275
  clearTimeout(pending.timeoutId);
25174
25276
  pendingRequests.delete(id);
25175
- pending.reject(new Error(`${why} before ${pending.method} answered. Check the editor tab, then call get_timeline to see whether the edit landed.`));
25277
+ pending.reject(unsettled(`${why} before ${pending.method} answered. Check the editor tab.`));
25176
25278
  }
25177
25279
  }
25178
25280
  function explainTabError(error2) {
@@ -25313,7 +25415,7 @@ function failUpstream(why) {
25313
25415
  for (const [id, p] of upstreamPending) {
25314
25416
  clearTimeout(p.timeoutId);
25315
25417
  upstreamPending.delete(id);
25316
- p.reject(new Error(`${why} before ${p.method} answered.`));
25418
+ p.reject(unsettled(`${why} before ${p.method} answered.`));
25317
25419
  }
25318
25420
  }
25319
25421
  function connectUpstream() {
@@ -25371,7 +25473,7 @@ function sendUpstream(method, params, projectId) {
25371
25473
  const id = crypto2.randomUUID();
25372
25474
  const timeoutId = setTimeout(() => {
25373
25475
  upstreamPending.delete(id);
25374
- reject(new Error(`Request timed out: ${method}`));
25476
+ reject(unsettled(`Request timed out: ${method}`));
25375
25477
  }, REQUEST_TIMEOUT_MS);
25376
25478
  upstreamPending.set(id, { resolve, reject, timeoutId, method });
25377
25479
  ws.send(JSON.stringify({ id, method, params, ...projectId ? { projectId } : {} }));
@@ -25385,7 +25487,7 @@ async function sendToBrowser(method, params = {}, projectId) {
25385
25487
  const id = crypto2.randomUUID();
25386
25488
  const timeoutId = setTimeout(() => {
25387
25489
  pendingRequests.delete(id);
25388
- reject(new Error(`Request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s: ${method}. Call get_timeline to see whether it landed.`));
25490
+ reject(unsettled(`Request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s: ${method}.`));
25389
25491
  }, REQUEST_TIMEOUT_MS);
25390
25492
  pendingRequests.set(id, { resolve, reject, timeoutId, socket: tab.ws, method });
25391
25493
  tab.ws.send(JSON.stringify({ id, method, params, projectId: tab.projectId }));
@@ -25492,7 +25594,8 @@ var LOCAL_TOOLS = [
25492
25594
  continueOnError: { type: "boolean", description: "Run the remaining steps after one fails." },
25493
25595
  rollback: { type: "boolean", description: "Take a checkpoint first and restore it if any step fails, so a run either lands whole or not at all. Cannot be combined with continueOnError." },
25494
25596
  projectId: { type: "string", description: "Run every step on this project (from list_connected_projects). Use it when several agents edit different tabs at once; a step's own projectId wins." },
25495
- expectedSequenceId: { type: "string", description: "Run every step that takes expectedSequenceId on this sequence (from list_sequences), open or not. Use it when several agents edit different sequences at once; a step's own expectedSequenceId wins." }
25597
+ expectedSequenceId: { type: "string", description: "Run every step that takes expectedSequenceId on this sequence (from list_sequences), open or not. Use it when several agents edit different sequences at once; a step's own expectedSequenceId wins." },
25598
+ opId: { type: "string", description: "Only to retry a run that failed or timed out: the opId its reply or error gave, with the same calls in the same order. Steps that already finished are answered without running again." }
25496
25599
  },
25497
25600
  required: ["calls"]
25498
25601
  }
@@ -25505,6 +25608,8 @@ for (const [alias, name] of Object.entries(TOOL_ALIASES)) {
25505
25608
  const tool = BROWSER_TOOLS_BY_NAME.get(name);
25506
25609
  if (tool) BROWSER_TOOLS_BY_NAME.set(alias, tool);
25507
25610
  }
25611
+ var PROJECT_FREE_TOOLS = /* @__PURE__ */ new Set(["create_project", "begin_request", "record_choice", "set_style_brand"]);
25612
+ var VIEW_TOOLS = /* @__PURE__ */ new Set(["switch_project", "open_sequence", "playback_control", "set_playhead", "set_tool"]);
25508
25613
  var SAVES_FRAMES = new Set(
25509
25614
  BROWSER_TOOLS.filter((t) => "saveToDisk" in (t.inputSchema.properties ?? {})).map((t) => t.name)
25510
25615
  );
@@ -25516,7 +25621,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
25516
25621
  ]
25517
25622
  }));
25518
25623
  async function callTool(name, args) {
25519
- const { projectId: rawProjectId, ...forwarded } = args;
25624
+ const { projectId: rawProjectId, opId: rawOpId, ...forwarded } = args;
25520
25625
  const projectId = typeof rawProjectId === "string" && rawProjectId ? rawProjectId : void 0;
25521
25626
  switch (name) {
25522
25627
  case "get_design_guide":
@@ -25544,16 +25649,40 @@ async function callTool(name, args) {
25544
25649
  if (!tool) {
25545
25650
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
25546
25651
  }
25547
- const saveToDisk = SAVES_FRAMES.has(name) && forwarded.saveToDisk === true;
25548
- if (saveToDisk) delete forwarded.saveToDisk;
25652
+ const saveToDisk = SAVES_FRAMES.has(name) && forwarded.saveToDisk !== false;
25653
+ if (SAVES_FRAMES.has(name)) forwarded.saveToDisk = false;
25654
+ if (name === "get_operation") forwarded.opId = rawOpId;
25549
25655
  const invalid = validateArgs(tool, forwarded);
25550
25656
  if (invalid) throw new McpError(ErrorCode.InvalidParams, invalid);
25551
25657
  const target = projectId ?? chosenProjectId;
25552
- if (name === "import_local_file") return importLocalFile(forwarded, target);
25553
- const result = await sendToBrowser(tool.method, forwarded, target);
25658
+ if (!target && !READ_ONLY_TOOLS.has(tool.name) && !PROJECT_FREE_TOOLS.has(tool.name)) {
25659
+ const open = connectedProjects();
25660
+ if (open.length > 1) {
25661
+ throw new McpError(
25662
+ ErrorCode.InvalidParams,
25663
+ `${open.length} projects are open, so ${tool.name} needs projectId to say which one it edits. Open: ${open.map((p) => `${p.name ?? "untitled"} (${p.projectId})`).join(", ")}. Pass projectId on the call (or once on run_tools), or call use_project first.`
25664
+ );
25665
+ }
25666
+ }
25667
+ if (!READ_ONLY_TOOLS.has(tool.name) && !VIEW_TOOLS.has(tool.name)) {
25668
+ forwarded.opId = typeof rawOpId === "string" && rawOpId ? rawOpId : `op-${crypto2.randomUUID().slice(0, 8)}`;
25669
+ }
25670
+ if (name === "import_local_file") return withOpIdHint(importLocalFile(forwarded, target), forwarded.opId);
25671
+ const result = await withOpIdHint(sendToBrowser(tool.method, forwarded, target), forwarded.opId);
25554
25672
  const withFile = saveFileToDisk(result);
25555
25673
  return saveToDisk ? saveFramesToDisk(withFile, name) : withFile;
25556
25674
  }
25675
+ async function withOpIdHint(call, opId) {
25676
+ try {
25677
+ return await call;
25678
+ } catch (err) {
25679
+ const message = err?.message ?? String(err);
25680
+ if (typeof opId !== "string" || !err?.unsettled) throw err;
25681
+ throw new Error(
25682
+ `${message} The edit may still finish. It is operation ${opId}: call get_operation with opId "${opId}" to see whether it did, or repeat this exact call with opId "${opId}" \u2014 the tab runs an opId once, so the retry cannot make anything twice.`
25683
+ );
25684
+ }
25685
+ }
25557
25686
  async function importLocalFile(args, target) {
25558
25687
  const local = await callLocal("find_local_file", { filename: args.filename }, target).catch((err) => {
25559
25688
  if (/^Several files/.test(err?.message ?? "")) throw err;
@@ -25582,6 +25711,7 @@ async function runTools(args) {
25582
25711
  const images = [];
25583
25712
  const refs = /* @__PURE__ */ new Map();
25584
25713
  let failed = 0;
25714
+ const runOpId = args.rollback ? null : typeof args.opId === "string" && args.opId ? args.opId : `op-${crypto2.randomUUID().slice(0, 8)}`;
25585
25715
  let checkpointId;
25586
25716
  if (args.rollback) {
25587
25717
  const made = await callTool("create_checkpoint", withRunProject({ name: "run_tools rollback" }, args.projectId));
@@ -25592,7 +25722,7 @@ async function runTools(args) {
25592
25722
  const tool = typeof step?.tool === "string" ? step.tool : "";
25593
25723
  try {
25594
25724
  const stepArgs = resolveRefs(step.args && typeof step.args === "object" ? step.args : {}, refs);
25595
- const raw = await callTool(tool, withRunSequence(withRunProject(stepArgs, args.projectId), args.expectedSequenceId, BROWSER_TOOLS_BY_NAME.get(tool)));
25725
+ const raw = await callTool(tool, withRunOp(withRunSequence(withRunProject(stepArgs, args.projectId), args.expectedSequenceId, BROWSER_TOOLS_BY_NAME.get(tool)), runOpId, i));
25596
25726
  if (typeof step.as === "string" && step.as) {
25597
25727
  if (raw && typeof raw === "object" && !Array.isArray(raw)) {
25598
25728
  const { _image, _images, ...kept } = raw;
@@ -25631,6 +25761,7 @@ async function runTools(args) {
25631
25761
  return {
25632
25762
  ok: failed === 0,
25633
25763
  results,
25764
+ ...failed && runOpId ? { opId: runOpId, retry: `Repeat this run_tools call with opId "${runOpId}": finished steps answer from the tab without running again.` } : {},
25634
25765
  ...rolledBack !== void 0 ? { rolledBack } : {},
25635
25766
  ...images.length ? { _images: images } : {}
25636
25767
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goatedit-mcp",
3
- "version": "1.1.19",
3
+ "version": "1.1.21",
4
4
  "description": "Local MCP server for the GoatEdit video editor: drive your open editor tab from Claude Code or any MCP client",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",