Repository navigation
Releases: heristop/leclap
Release list
ffmpeg-video-composer 3.2.0
Added
- Layer blend modes:
inputs[].options.blend(normal,multiply,screen,overlay,add,darken,
lighten,difference) mixes an image, animation or html layer into the picture inside its own alpha,
with CSS mix-blend-mode formulas in RGB: transparent areas leave the picture untouched and half-transparent
edges mix by their alpha. Lowered toblendover a transparent copy of the frame. - Motion blur:
motionBlur(trueor{ shutter 0-360, samples 2-16 }) on layers with position, scale or
rotate tracks and on travelling kinetic blocks. Only the moving frames are supersampled on a sub-frame
shutter clock and averaged withtmix; resting frames and the background stay sharp. Bounded per element
(motion_blur_capped). - The on-device engine adds
blendandtmix(LGPL); a build without them composites normally or draws
sharp, with ablend_unavailable/motion_blur_unavailablecompile warning (validation reports them only
whengetMotionWarningsis given the target build's capabilities). Validation also advises
motion_blur_staticandmotion_blur_ignored. - Sample
blend-motion-blurand motion-review fixtureslayer-blendandmotion-blur. - Six more packaged samples:
html-motion(animated html cards),vector-shapes,line-draw,fx-burst,
fx-liquidandfx-glitch-iris-sheen.layer-keyframesandkinetic-typeblur their fast moves with
motionBlur, andeffects-touradds shaped-iris and bands beats plus blend-mode, mask, shape and motion-blur
beats. 59 samples in all. - Masks and track mattes: an image, animation or html layer takes
options.mask, and a video or project_video
section takesoptions.mask. A mask is ashape(rectwithradius,ellipse, or apathdof
M/L/H/V/C/Q/Z, 4000 characters at most) drawn once at compile time into a grey image, or alayer, another
layer of the section read as a track matte (modealphaorluma; the matte layer is not drawn).invert
shows outside the mask,feathersoftens its edge (px), andanimatemoves a shape with the layer tracks
(x/yoffsets,scale,scaleX,scaleY,rotateaboutorigin). A section mask showsbackground(a
colour token) outside; layers and text draw on top. The element is drawn on a copy of the frame as before,
the copy takes the mask as its alpha and goes back over the frame, so a layer keeps its own transparency
(split,format,lutyuv,overlay,alphamerge,colorchannelmixer,gblur, all on device). Without
alphamergea shape falls back to its resting box and a track matte draws both layers (mask_unavailable).
New validation codes:mask_layer_unknown,mask_layer_self,mask_layer_shared,mask_layer_chained,
mask_not_layer,mask_with_clip,mask_with_composite(a mask next to a non-normalblendor a
motionBlur),mask_path_invalid,mask_too_large. - Clip boxes: a kinetic block takes
clip("block", its own resting box;"line", one box per line; or
{ x, y, w, h }in px) and an image, animation or html layer takesoptions.clip(a box). The element only
shows inside the box, so arisereveals out of an invisible slot above an underline, on any background,
footage included, without a background-coloured panel. The frame is split, the element is drawn on one copy
exactly as before, and the copy is cropped to the box and laid back over the frame (split,crop,
overlay, all on device). Elements without a clip compile to the same filters as before. bands, a designed transition:countparallel colour bands (2–8, default 4) atangledegrees (default 30) sweep in along their length, staggered (stagger, default 0.06 s), cover the frame at the midpoint,
then carry on and uncover the next scene.colorsdefault to the theme's brand, accent, accent2 and fg.
Each band is a rectangle turned once byrotateand held byloop; per frame only overlay positions move.- Iris shapes: a designed
iristakesshape(circleby default, unchanged, orstar,heart,
rounded-square). A shape opens fromoriginon theeasethrough a reveal field drawn once at compile
time (per pixel, the scale the shape must reach to cover it), compared with the eased level on a half-size
xfademask like the circle's. - Zoom through a window: a designed
zoom-throughtakesthrough, a letter's counter
({ text, glyph?, font?, size?, x?, y? }, laid out exactly as the kinetic block with the same values draws
it: the O of ZOOM) or a shape ({ shape, size, x?, y? }). The window is a compile-time matte, read from the
font's TrueType outlines; the outgoing scene and the matte run through the same zoom chain about the
window's centre, so the letter and its hole stay locked together, and the next scene shows through the
hole until it covers the frame. A window that cannot be drawn falls back to the plain zoom-through with a
warning. Shaped irises and windows add their matte image as an input of the transition assembly. - Reference structure:
analyzeStructurereads a reference clip's format (shots with their length, the
transition into each (cut, dissolve →fade, dip →fadeblack), light or dark ground, where the main
element sits on a 3×3 grid and motion) plus its rhythm, from the frames the style analyzer already
samples.structurePromptRulesturns it into binding generation rules (24 shots listed at most) and
structureDriftholds a template to it.analyzeStructureFiledecodes a clip on Node. Format only: never
the reference's subjects, logos or text. - Brand kits:
fetchBrandKit(url)reads a website's name, colours (theme-color, brand-named custom properties, the most used declared colours), fonts (heading, body, code) and logo over HTTPS from public addresses only, with size, time and redirect caps;saveBrandKitwrites the kit and keeps it when the logo fails (logoError);brandTheme(kit)turns it into aglobal.theme(roles with WCAG contrast, the nearest bundled fonts plus the exact families asFontRefsuggestions) andbrandPromptRulesinto binding generation rules. - Animated HTML cards: elements tagged
data-motion="name"move on the input'smotionmap
({ name: { x, y, scale, rotate, opacity, stagger } }, keyframes as inoptions.animate), so a card's tag,
lines and chips can come in one after another. The card is drawn at the output fps over the animated span
only, held frames drawn once, joined into one animated PNG and composited with the overlay path (no new
filter; theapngdemuxer is on every backend), holding its final frame. Bounded (8 s, 240 frames, 500
megapixels:html_motion_too_long),html_motion_unmatchedfor a name no element carries, and a still in
its final state (html_motion_static) when the frames can't be produced. Sample:
examples/motion-design/html-motion.json. - A
shapegraphic: a filled vector shape for a sticker star, a heart, an arrow pointing at a detail, a blob
behind a headline or an icon. The outline is apreset(star,polygon,heart,arrow,blobwith a
seed,rectwith rounded corners) laid in a box (x,y,width,height, px or fractions of a
target), or an SVGdof M/L/H/V/C/Q/Z, mapped into the box by aviewBoxfor icons. Star, polygon,
heart and blob keep their proportions in portrait and square. It takes acoloror a linear / radial
gradient,fillRule(nonzero,evenodd), and astrokethat is either part of the shape or drawn on
(stroke.draw, the path graphic's reveal, then the fill fades in). It moves with the layer keyframe tracks
(animate:x,y,scale,scaleX,scaleY,rotate,opacity, springs included;originis the
pivot), or fades in overduration. The shape is rasterised once at compile time (shape:images, the same
bytes on every platform) at its on-screen size, twice that for a growing scale track, never past 4 MP
(the render log reportsshape_downsampled; advisories are logged at compile time andvalidatedoes not rasterise). It renders on device with the filters keyframed image layers use. Advisories:
shape_too_large,shape_unavailable,shape_draw_unavailable,shape_target,shape_empty,
shape_color. - Validation:
invalid_keyframesfor a shape's tracks (keys out of order, opacity outside 0..1, scale outside
0..10, relative values on scale or opacity). - Keyframe tracks on image, animation and html layer inputs:
inputs[].options.animatetakesx,y,
scale,scaleX,scaleY,rotateandopacitytracks, with the same{ t, v, ease? }keys, easings,
springs, tokens and time references as the drawtext tracks. Cards, badges and icons can now pop in with an
overshoot, squash and stretch, flip (scaleX1 → 0 → 1 across two layers), swing like a needle or drift in
parallax, instead of only entering withoptions.motion.options.origin(centerby default, or an edge
or corner) is the point that stays put while the layer scales or turns.x/ytake pixels or offsets from
options.position;rotatetakes degrees, absolute or relative tooptions.rotation;opacitymultiplies
options.opacity. A track overrides the same property ofoptions.motion. Scale and rotate tracks need the
layer box: an html layer has it, an image or animation needsoptions.scaleas"W:H"pixels. Every filter
runs on the layer's own frame and is in the on-device build (fps,scale,rotate,colorchannelmixer,
overlay); a property without a track adds nothing, and templates withoutanimatecompile to the same
commands as before.- Validation:
invalid_keyframesfor layer tracks out of order, an opacity outside 0..1 or a scale outside
0..10, relative values on scale or opacity, a scale or rotate track on a layer whose size is unknown, and
tracks on an input that is not a layer; an unknown track name is a...
- Validation:
@leclap/mcp 0.7.0
Added
get_template_schemaandget_motion_catalogteach layer blend modes (options.blend) and motion blur
(motionBluron layers and kinetic blocks);validate_templateadvisesmotion_blur_static
andmotion_blur_ignored. A build withoutblendortmixfalls back at render time (a normal composite,
or sharp) with ablend_unavailable/motion_blur_unavailablewarning in the compile log; validate_template
does not know the target build, so it does not report those two.get_template_schemaandget_motion_catalogteach masks and track mattes (options.mask,
compositing.masks).extract_structure: a reference clip's format (shots, rhythm, transitions, grounds, layout) as binding
rules, from at most 480 sampled frames.get_template_schemaandget_motion_catalogteach animated HTML cards:data-motionnames, the input's
motionmap with its stagger, the budget (html_motion_too_long,html_motion_unmatched) and a
staggeredListrecipe.get_template_schemaandget_motion_catalogteach the engine'sshapegraphic: presets or an SVGdwith
viewBox, colour or gradient fill, stroke, layer tracks or a drawn-on outline, and itsshape_*advisories.--allow-raw-filters/LECLAP_MCP_ALLOW_RAW_FILTERS(off by default) trusts a template's raw filters for
trusted local use;get_capabilitiesreports which mode the server runs in, and the template guide says so.extract_brand(behind--allow-network/LECLAP_MCP_ALLOW_NETWORK): a website's brand kit as a theme and binding rules, saved under the media dir.get_template_schema's guide describes layer keyframe tracks (options.animatewith x/y/scale/scaleX/
scaleY/rotate/opacity andoptions.originon image, animation and html inputs), thepathgraphic and the
dripandbandstransitions, clip boxes, shaped iris andzoom-throughwindows.get_motion_cataloglists theburstandgoofx and the art-direction rules for the
new effects;validate_templateandcompose_videoaccept everythingffmpeg-video-composer3.2.0 adds.
Changed
- Raw filters in agent templates are allow-listed by the engine: a filter outside the list is a
filter_not_allowedvalidation error instead of a render. A drawtextfontfileis a bare font name; fonts
from the media dir go throughglobal.fonts. - Requires
ffmpeg-video-composer^3.2.0.
Fixed
validate_template,compose_videoandrender_previewrefuse a template whose string fields (a
transition colour, for instance) carry FFmpeg filter syntax that opens files or URLs, not only the filter
valueandvalues. Ordinary copy with semicolons or commas is unaffected.- The template guard also sees through backslash-escaped filter names and refuses the command, socket and
model-loading filters, and file options chained into a filter. - The template guard also refuses the LUT, SOFA, mask, OCR, speech and neural-network filters that load files,
and more file-capable filter options. - The template guard also refuses a protocol hidden behind an escaped colon (
'file\:/…','http\://…'), and
the server warns on stderr at start-up when--allow-raw-filtersis on.
@leclap/cli 0.6.0
Added
leclap initwrites agent guidance: anAGENTS.mdpointer (imported by a one-lineCLAUDE.md) and.agents/skills/authoring-leclap-videos/SKILL.md,
with or without the.mcp.jsonoption. It covers the validate/render loop (through the project's package
manager, sinceleclapis a devDependency), the sugar tier before rawfilters[], the re-encode cost model and
the traps that never surface as errors, and points atllms.txtinstead of restating the schema.leclap reference <clip>prints a reference clip's format as binding rules (--json,--out);
--check template.jsonexits 1 on drift.leclap compare --reference <clip>adds the reference's frame
as the first tile.leclap render --allow-raw-filtersandleclap verify --rerender --allow-raw-filterstrust a template's
raw filters (trusted local templates only). Without it, raw filters are allow-listed by the engine and a
template that uses a filter outside the list fails withfilter_not_allowed.leclap brand <url>writes a brand kit, a theme and binding rules.leclap sampleslists 59 samples:layer-keyframesanimates cards and shapes with layer keyframe tracks
(pop-in, squash and stretch, flip, needle, parallax),blend-motion-blurmixes layers with blend modes and
blurs their motion,maskscuts layers and a video section with shape masks and track mattes, and
html-motion,vector-shapes,line-draw,fx-burst,fx-liquidandfx-glitch-iris-sheenjoin the list.leclap renderandleclap validatetake everythingffmpeg-video-composer3.2.0 adds: layer keyframe
tracks (options.animate,options.origin), thepathandshapegraphics, animated html cards
(data-motionwith the input'smotionmap), layer blend modes (options.blend) andmotionBlur, masks and
track mattes (options.mask), theburstandgoofx, thedripandbandstransitions, clip boxes
(clip,options.clip), shaped iris,zoom-throughwindows, glitchtear, irisorigin/ease/rimand
sheenprofile: "gloss".
Changed
- Requires
ffmpeg-video-composer^3.2.0.
Fixed
leclap verify --rerendervalidates the manifest's template before rendering it again, instead of
skipping validation.
ffmpeg-video-composer 3.1.0
Added
- Template fonts for HTML layers.
global.fontsdeclares the faces a template brings,
{ family, src, weight?, style? }, one entry per weight or style; an HTML layer selects one with
font-familyin itscss, and a declared family wins over a bundled family of the same name.srcis a
TrueType (.ttf), OpenType (.otf) or WOFF (.woff, unpacked to TrueType by the engine's own pure-JS
inflate, no new dependency) file, or a base64
data:URI, at most 8 MB. WOFF2 is refused with the command that converts it (Satori and HarfBuzz read
neither WOFF2 nor its Brotli tables). Drawn text (drawtext, kinetic, captions) keeps the font registry. ProjectConfig.fontDirs(Node): read-only directories asrcpath resolves in, before the assets dir. A
relativesrcis looked up in each in turn, an absolute one must lie inside one of them, and symlinks are
resolved before the check. Template fonts are never fetched over the network, and the temp and build dirs
other staged media may come from are not searched.- A layer drawn with a template font is named by the hash of the font's bytes: the same font file gives the
same frames, and a changed file under the same name is drawn again rather than reused. - Validation:
font_woff2_unsupportedandfont_formatfor asrcthat is WOFF2 or no font,font_too_large
for an oversized data URI, and thefont_unusedadvisory for a declared family no HTML layer selects.
templateFontErrors(descriptor, { assetsDir, fontDirs })(Node entry) opens the files the way a render
reads them and reportsfont_not_found,font_unreadable,font_format,font_woff2_unsupportedand
font_too_large.loadTemplateFontsandtemplateFontSpecsare exported from the Node and browser
entries;renderHtmlLayerPreviewtakes the loaded faces astemplateFonts.
Changed
- CSS
@font-faceis still dropped, and itshtml_unsupported_cssfinding now points atglobal.fonts.
html_font_unknownsays the family is neither declared inglobal.fontsnor in the registry. - The packaged evidence samples (
house-evidence,pr-evidence,before-after) take LeClap's evidence look:
ink cards, BEFORE in rose and AFTER in lavender, Oswald headlines in the brand gradient and engine motion
(kineticscrambleandcascade, drawn rules, asheen).house-evidencenow records onesidebyside
clip, BEFORE and AFTER composed side by side, instead of six sections;pr-evidenceandbefore-after
keep their sections and gain aticketvariable.
Fixed
- Speed ramps no longer judder at constant speed. The ramp's
setptsnow rounds to whole ticks instead of
truncating, so a source frame that lands on a half tick (a 120 fps clip at 1x) stops falling either side of
the frame-rate conform with float noise, which cadenced 5, 3 source frames instead of 4, 4. - Files read on the phone (React Native) keep their last bytes: the Expo filesystem adapter decoded base64 with
its own helper, which turned the final one or two bytes of any file whose size is not a multiple of 3 into
0xFF. An image inside an HTML layer (a JPEG losing its end marker) drew blank on the phone, and fonts or
other staged files could be cut the same way. The adapter now uses the engine's shared base64 helpers. - A muted
project_videosection (muteSection: true) with layers (HTML, image or animation inputs,@video
maps, chroma key) no longer fails with "Stream specifier ':v' … matches no streams". The muted section
puts its silent audio input first, so the recorded clip is input 1, but the overlay graph still read the
video from input 0 and numbered the layers from 1. - The phone's HTML layer page (
dist/html-rasteriser.html) loads on older WebViews again: it called
Promise.withResolvers(Chrome 119, Safari 17.4), so every HTML layer failed on an Android System WebView
before 119 or iOS before 17.4. The page now supports Android System WebView 87+ and iOS 16.4+: its build
targets them and it polyfillsArray.prototype.atfor Satori where missing. Rendered bytes are unchanged. - The before/after wipe (and
splitpanes) no longer stutters. A still or clip pane arrived at the image
demuxer's 25 fps and was never conformed to the section's frame rate, so the wipe'scrop, timed on that
leg, drifted from theoverlayat the output rate: the revealed picture crept a few pixels a frame, then
snapped back every 6 frames at 30 fps. Media and clip panes now run at the section frame rate. - Boxed caption plates draw their rounded corners row by row instead of in three coarse steps, so the corners
no longer look pixelated when the frame is scaled up. Each plate line stays within about 25drawbox
filters: an anti-aliased version cost 77 to 193 per line and made long boxed transcripts render many times
slower, so it was not kept.
@leclap/mcp 0.6.0
Added
global.fontssupport.--fonts-dir/LECLAP_MCP_FONTS_DIRadds a read-only directory template fonts
resolve in, before the media dir; nothing outside the two is read (realpath-checked) and fonts are never
fetched.validate_templateandcompose_videorefuse a declared font that is missing, outside those
dirs, not a TTF/OTF/WOFF font, WOFF2 or over 8 MB, naming the entry and the fix.get_template_schemadescribesglobal.fontsin its schema and guide.
Changed
- Requires
ffmpeg-video-composer^3.1.0.
@leclap/cli 0.5.0
Added
global.fontssupport:leclap renderandleclap validateresolve a template font'ssrcagainst
the template's own directory first, then each--fonts <dir>(repeatable), then the assets dir, and read
nothing outside them.leclap validateopens the declared files and fails on one that is missing,
outside those dirs, not a TTF/OTF/WOFF font, WOFF2 or over 8 MB.
Changed
- Requires
ffmpeg-video-composer^3.1.0. leclap samples: the three evidence samples (house-evidence,pr-evidence,before-after) take LeClap's
evidence look;house-evidencenow takes onesidebysideclip, and the other two aticketvariable.
ffmpeg-video-composer 3.0.0
A motion, effects, footage, audio and agent-tooling release: HTML layers, composed sound effects, typed
template fields, auto-captions, per-format compositions and a determinism contract. Validation is stricter
and renders are no longer byte-identical to 2.5.0, hence the major version.
Upgrading from v2? See the migration guide.
Added
-
HTML layers, on Node, in the browser and on the phone. An
inputs[]entry oftype: "html"takeshtml,
css,widthandheightand lays out a card, badge, price tag or stat row in a flexbox subset of CSS
(tag,.classand descendant selectors). The markup is sanitised (no scripts, iframes, forms, SVG, event
handlers or links),{{ name }}placeholders are filled HTML-escaped from typed fields, variables and form
values, and$color.*/$font.*tokens resolve incssandhtml. Fonts come from the registry
(variable fonts are pinned to static weights with HarfBuzz; layers of one section may share a font); images
must be template assets or PNG/JPEG data URIs. Satori, resvg and HarfBuzz draw the layer at 2× into a transparent PNG named by content hash
(html:<hash>, cached per process), which composites as a still image, soposition,scale,startand
motionwork unchanged. One pipeline (core/html/satori-raster.ts) runs on every host, so a layer's PNG is
byte-identical everywhere; only where its WebAssembly comes from differs:- Node reads it from
node_modules. - The browser engine (
compileBrowser) takes it from the host throughBrowserCompileOptions.loadHtmlWasm
(the web app serves it under/html-engine/<version>/); the engine never fetches it from a third party.
It loads on the first HTML layer, in lazy chunks.renderHtmlLayerPreview(request, { loadHtmlWasm })
draws one layer with the advisories its render reports, for a live preview. - Hermes has no WebAssembly, so the React Native entry takes a rasteriser from the host
(registerHtmlRasteriser). The build shipsdist/html-rasteriser.html, one self-contained page (5.2 MB,
the pipeline and its WebAssembly inlined) that a hidden WebView runs, spoken to with
createRasterSession/readRasterReply(JSON messages, PNG and fonts as base64, each font sent once per
page).
A host with no rasteriser registered (a browser compile without
loadHtmlWasm, a React Native host that
registered none) refuses the template withhtml_unavailable; validators that only check templates (the
MCP server, the CLI) accept HTML layers. Validation also fails onhtml_too_large(a side over 1920 px).
Advisorieshtml_unsupported_css,html_unsupported_markup,html_font_unknown,html_missing_fieldand
html_overflow(measured in the Node geometry checks);motionCatalog().htmllists the subset and four
recipes. Samples:html-card(a listing reel),html-testimonial(a Reels quote card),html-speaker(a
conference lower third) andhtml-stats(a square stat dashboard), inexamples/motion-design/. - Node reads it from
-
Composed sound effects. An
sfxcue takes asoundinstead of anid: layers oftone,noise,
strikeandsilenceshaped by envelopes, filter chains, glides, drive, pan and sequences, with
whole-sound saturate/crush/room/echo, bounded to 4 s and 8 layers. A pure TypeScript synth renders it,
seeded byglobal.seedand the cue path, tobuild/sfx/<hash>.wav, mixed like a library file: the same
sound and seed give the same file on a given platform, while Node, browsers and Hermes can differ in the
last bit (each JavaScript engine computesMath.sinandMath.expits own way). A note shorter than its
attack plus release (a tick, a fast roll) stays audible, and jittered, accelerating rolls keep every hit.
Every library sound is also a recipe (SOUND_PRESETS);sound.presetvaries one bypitch,length,
brightnessandroom, whileidcues keep playing the shipped files. A sound's notes add up to at most
32 s of audio, so its render cost stays bounded. Advisoriessound_silent,sound_clipped,
sound_harsh,sound_muddy,sound_long,sound_repeatedandsound_overlap;
motionCatalog().audio.compose;renderSound,analyzeChannels,soundSpecandSoundSchemaexports. -
Typed template fields:
global.fieldsdeclares a template's inputs (map or list of{ name, type, default?, required?, maxLength?, min?, max?, options?, label?, description? }, types text, color, url, media, number,
enum and time). Values come fromProjectConfig.fields, thendefault; each is coerced by its type and
filled into{{ name }}after partial expansion — a whole-string placeholder takes the typed value, so
"duration": "{{ HOLD }}"lands as a number — then the slot's own schema judges it. A render fails before
encoding on a missing required or ill-typed value;validateTemplate(t, { fields })checks strictly,
withoutfieldsit probes. New advisories (getFieldWarnings, also ingetMotionWarnings):
field_undefined,field_unused,field_type_mismatch,field_missing_required. Node entry exports
resolveFields,assertFieldsResolved,coerceFieldValue(pluggable coercers andencode),
declaredFields,fieldAdvisoriesandresolveTemplate. Templates withoutglobal.fieldsare unchanged.
Colours follow FFmpeg's grammar (#rgb(a)andrgb()/rgba()normalised to#rrggbb(aa), names from
FFmpeg's list,@alphain 0–1); urls take http(s), data,media://or relative paths; a field value with
filtergraph separators is refused in a raw filter value. Validation without values returns the authored
descriptor; the resolved descriptor dropsglobal.fields, so resolution is idempotent. A form field bound
to a non-text declared field may omitmaxLength. -
Auto-captions:
subtitles.transcribe: { from?, language?, model? }asks for a section's speech as word
timings. Transcribe once, then pin: a Node resolve pass (whisper.cpp — thewhisper-clibinary with DTW word
alignment, else an FFmpeg built with--enable-whisper) replaces the request withsubtitles.wordsand records
engine, model, language, clip digest and date inmeta.resolved.transcripts; renders only read pinned words.
Word times are mapped from clip to section seconds throughclip,speed, speed ramps, freeze holds and kept
take windows. The Node compile resolves an unpinned request itself; the browser and on-device engines report
transcribe_unavailable. New advisoriestranscript_low_confidenceandtranscript_stale(the clip changed
since the pin), new errorinvalid_transcribe_source, and optionalconfidenceon words. Node exports
transcribeMediaFile,transcribeTemplate,ensureWhisperModel(models downloaded once on explicit opt-in to
~/.cache/leclap/whisper, SHA-256-verified, never bundled) and the platform-neutralmapTranscriptWords,
pinTranscript,transcriptSrt(also on the React Native entry). Pins also record a fingerprint of the
section's edits, andtranscript_edit_changedreports a pin whose clip, speed, trim, ramp or freeze changed
since.CompileReporter.signal(Node) cancels a build, transcription included. -
Template links (Node entry):
createBuilderLink,encodeTemplatePayload/decodeTemplatePayloadand
mediaToRebindcarry a template, compressed, in a builder URL's#t=fragment (never sent to a server),
listing the media only the source machine can read (local paths,file:,media://, unsupported schemes).
A base URL other thanhttps://leclap.devadds a warning, since that page can read the fragment. -
applyJsonPatch(doc, operations, { maxOps })andparsePointer(pointer)(Node entry): RFC 6902 JSON
Patch (add,remove,replace,move,copy,test) over RFC 6901 pointers (-append,~0/~1
escapes). Atomic (runs on a copy, the input is never mutated); rejects__proto__/prototype/constructor
segments and keys and anything deeper than 64 levels; failures throw aJsonPatchErrorwith acode, the
failing operation'sindexand itspath. -
templateRevision(template): stable SHA-256 of a template's canonical JSON (object key order ignored),
synchronous and platform-neutral; matches anode:cryptodigest of the same JSON byte for byte. -
findingLine,invalidTemplateTextandsummarizeErrors(Node entry): the plain-text renderings of
validation findings@leclap/mcpuses, now shared with other agent surfaces. -
Effects tour (
examples/motion-design/effects-tour.json): a six-minute tour of every motion effect in ten chapters (chapter 5 now shows every fx primitive), first in the Effects & editing showcase (51 samples). -
samples: neweffectscategory with 16 native samples (effects tour, FX pack, word captions, formats, kinetic fills, split layouts, right-to-left type, emoji type, beat grid, theme/roles/safe zones, footage editing, sound design, and four HTML layer samples: listing reel, testimonial, speaker card, stats dashboard). -
Determinism contract.
global.seedroots every procedural effect. A deterministic encoder profile
(bit-exact muxing, pinned libx264 threads) is applied to every command through one adapter tap; it is on
by default andProjectConfig.deterministic: falseopts out. A render manifest is delivered through
CompileReporter.onManifest. Raw filters that read the wall clock orrandom()raise the advisory
nondeterministic_expression(their renders are not reproducible). -
Motion system: physical springs, cubic-bezier, the named curve set,
steps()and point curves, all lowered to piecewise polynomials intwithin 0.1%.global.motion
tokens (springs, curves, durations, energy) and built-ins mirror the app's motion curves.animate
keyframe tracks (x,y,opacity,scale) on positioneddrawtext. The energy dial scales every
travel distance. Each section chain is confor...
@leclap/mcp 0.5.0
Removed
-
Breaking: four 0.4.0 tools are gone, merged into others or dropped, because every tool's name and
description is sent to the agent on every turn and overlapping tools dilute its choice. No aliases;
call the replacement:Removed Call instead pingthe MCP protocol pingrequestpatch_template { edits }edit_template { template, expectedRevision, effectProps: edits }list_samples { …filters }get_samples { …filters }get_sample { id }get_samples { id }Fourteen tools are always registered (seventeen with the Remotion opt-in):
analyze_music,
analyze_sound,compose_video,edit_template,extract_style,get_capabilities,
get_motion_catalog,get_samples,get_template_schema,open_in_builder,probe_media,
render_frames,transcribe_mediaandvalidate_template, plusget_effect_schema,
render_previewandrender_remotion_clipwith Remotion.
Changed
get_samplesreplaceslist_samplesandget_sample: withoutidit lists (it acceptscategory: "effects"; 51 packaged samples), withidit returns the sample and its template, plus a
partialCatalogsummary.validate_templatetakesinclude: ["resolved" | "timeline"], plusfieldsandformatfor them:
resolvedis{ descriptor, values }, the descriptorcompose_videowould render for the given
fields(a refused field value is still an error), andtimelineis the resolved section timeline.
Withoutinclude, validation is unchanged.- Template revisions (
expectedRevision/revision) and the validation finding text now come from the
engine (templateRevision,invalidTemplateText), so other surfaces compute the same revision for the
same JSON; values are unchanged. - Built on
ffmpeg-video-composer3:validate_template,edit_templateandcompose_videoreject
templates with unknown keys (unknown_key) and text a bundled font cannot draw (font_missing_glyphs),
which 0.4.0 accepted.
Added
- HTML layers (
inputs[].type: "html"):compose_videoandrender_framesdraw a card, badge or price
tag laid out in HTML and CSS and composite it like an image; its images must be template assets under the
media dir.validate_templatereportshtml_unsupported_css,html_unsupported_markup,
html_font_unknown,html_missing_fieldand a measuredhtml_overflow, and fails onhtml_too_large.
get_template_schemadocuments the input andget_motion_cataloglists the CSS subset and four layout
recipes underhtml. analyze_sound: renders ansfx[].sound(composed layers, or a library preset varied by pitch, length,
brightness and room) with the engine synth and returns its length, peak and RMS dBFS, raw
pre-normalisation peak, spectral centroid, energy above 8 kHz / under 250 Hz, attack time and the sound
advisories it raises, plus a spectrogram and a waveform PNG. An optionalcuepath
(sections.intro.sfx[0]) seeds the render exactly like the mix; a silent sound reports −120 dB and
sound_silent.- Typed template fields:
compose_videoandrender_framestakefields(declaredglobal.fields) as
strings, numbers or booleans and refuse a missing required or ill-typed value before rendering;
validate_templatelists the declared contract asfields, reportsfield_undefined,field_unused,
field_type_mismatchandfield_missing_required, and withinclude: ["resolved"]returns the
descriptor the given values produce or every value it would refuse. open_in_builder: returns ahttps://leclap.dev/studio/builder#t=v1.…link that opens the template in
the web builder for a person to edit, withmediaToRebind(local paths and uploads the browser cannot
read) andwarnings. The template travels compressed in the URL fragment, which browsers never send to a
server;baseUrltargets a locale prefix or a local dev server, with a warning offhttps://leclap.dev.transcribe_media: transcribe a local audio/video file's speech with whisper.cpp on the host → words, SRT,
language, mean confidence and the "pin, then review" advice.compose_videoresolvessubtitles.transcribe
before rendering. The model is never downloaded by the tool: the operator opts in once
(leclap transcribe --download-modelorLECLAP_WHISPER_DOWNLOAD=1).edit_template: an RFC 6902 JSON Patch over inline template JSON underexpectedRevision(stale →
revision_conflict), all-or-nothing and validated after applying; returns the template, its new
revisionandchangedPaths. It also takeseffectProps([{ section, props }], the former
patch_templateedits), applied after theoperationsin the same batch and checked by the effect
backend, and then returnschangedSections;operationsis optional wheneffectPropsis given. The
web builder's WebMCP tools share the name, operations and revision, along withget_template_schema,
get_motion_catalog,validate_templateandrender_frames.compose_videoruns the engine's output QC and returns it instructuredContent.qcwith a one-line
verdict.get_motion_catalogreturns the engine's motion catalog: kinetic typography presets with
defaults, exits, stagger orders, the easing grammar, built-in tokens, art-direction rules and a starter.
get_template_schemaand thecompose-videoprompt point agents at it.compose_videorenders with the deterministic encoder profile.validate_templatereturns every finding with its hint (structuredContent.errorswithhint,
suggestion,kind) and advisorymotionWarnings; failing section assertions are validation errors.get_motion_catalogalso returns genre doctrine, scene blueprints, per-preset verbs and guidance,
built-in themes, delivery platforms and the time-reference grammar; the registry manifest lists it.get_template_schemadescribesglobal.platform; thecompose-videoprompt adds motion pacing rules.- New always-registered tools:
extract_style(theme and style guide from a reference under the media
dir),analyze_music(beat grid and cues),get_capabilities(local FFmpeg capability report) and
render_frames(PNG frames, contact sheets, safe zones, variant and look grids). get_motion_catalogaccepts{ query, kind? }and returns ranked matches; a query that matches nothing
is appended to the catalog gap log (--catalog-gap-log/LECLAP_MCP_CATALOG_GAP_LOG, confined to the
output dir).compose_videoandrender_framesacceptformat; the template is resolved to that format before
validation, the sandbox guard and the render.validate_templatetakes it forinclude.validate_templateaddsfeatureWarningsfrom the local capability probe and reports
partial_compressed.probe_mediareportshdr,colorPrimaries,colorTransfer,bitDepth,vfrandrotation.- The
get_template_schemaguide covers formats, footage editing, subtitles, voice/automation/sfx, music
timing, motion roles and section purpose, trails, the new graphics, whips, lower-third styles, fills,
layouts, right-to-left text and emoji. get_motion_cataloglists thetype: "fx"primitives (parameters, defaults, design intent, reduced
motion) underfx, and the library APNGs undersampleswith the primitives that replace them;kind: "fx"searches them.get_template_schemaand thecompose-videoprompt steer agents to compose motion
from these primitives, tuned to the brief, andvalidate_templatereturns the sameness lint
(fx_untuned,effect_repeated,library_animation_sample,effect_off_theme,decor_overload).
@leclap/cli 0.4.0
Added
- Typed template fields:
leclap render --set name=value(repeatable) fills a declaredglobal.fields
entry, coerced to its type (--fieldstill works;--setwins);leclap resolve <template> [--set …]
prints the descriptor a render would see and exits 1 listing every value it would refuse. leclap studio <template> [--open] [--base <url>] [--json]prints a link that opens the template in the
web builder. The template travels compressed in the URL fragment, which browsers never send to a server;
media only this machine can read are listed to re-bind.--openstarts the platform's browser opener;
flags may come before the template.leclap transcribe <template|media>: pins everysubtitles.transcriberequest of a template into
subtitles.wordswith whisper.cpp, locally (--section,--language,--model tiny|base|small,--force,
--out,--video section=path,--json), or prints a media file's words / SRT (--json,--srt).
--download-modelfetches the model once (checksum-verified);leclap render --download-modelallows the
same when a render resolves an unpinned request.leclap samples list --category effects; the catalog now has 51 samples.leclap render --qcprints output QC findings and exits non-zero on a failing check (--json
includes the report);--cache <dir>reuses unchanged sections across renders.--outputis written
atomically and refused when it equals the template or a--videoinput.leclap render --manifestwrites<output>.manifest.json. Renders use the deterministic encoder
profile by default (--no-deterministicturns it off).leclap verify <manifest> [--rerender]checks a video against its render manifest, or re-renders the
recorded template and compares the template, asset, filtergraph and output digests.leclap validateprints a→ hintline under each error that has a known fix, plus advisory motion
pacing findings that never change the exit code;--jsonincludeshint,suggestion,kindand
motionWarnings.leclap render --format <name>renders one composition of a template withformats;--formats all|a,b
renders each to<output>-<format>.mp4.leclap style <image|clip> [--json] [--out style-guide.md]derives a theme and style guide from a reference.leclap beats <audio> [--json]measures tempo, the beat grid, confidence and drop/build/end cues.leclap init --studio <dir>scaffolds a production folder;leclap studio status|passtracks its gates.leclap diagnosereports which features the FFmpeg build can run, with fixes;--jsonprints the
capability report.leclap snapshot,leclap compareandleclap timeline [--json]look at a template's frames, compare
variants and list where everything sits; all take--format.
Changed
- Built on
ffmpeg-video-composer3:leclap validateandleclap renderreject templates with unknown
keys (unknown_key) and text a bundled font cannot draw (font_missing_glyphs), which 0.3.0 accepted.
ffmpeg-video-composer 2.4.0
Added
CompileReporter.onError(error): called with the cause whencompile()resolvesnull, so a
host can report why a render failed instead of a generic "no output": the error that stopped
the director (a failed section, an FFmpeg command it rejected, a segment it could not probe) or
the validation errorcompile()caught itself. A failure the director reported as a bare
string arrives as that string, not JSON-quoted.compileBrowsertakes aloadFFmpegCoreoption, for a host that serves the ffmpeg.wasm
core itself rather than having every visitor fetch it from unpkg (the LeClap web app now
self-hosts it, which also keeps rendering available offline).FFmpegWasmAdaptertakes the
same loader as its second constructor argument, andFFMPEG_CORE_VERSIONnames the
@ffmpeg/coreversion to serve. Without the option nothing changes: that pinned version,
0.12.10, still comes from unpkg.- Any Google Fonts family, by name. A text
fontnow also accepts
{ family, weight?, style? }(e.g.{ "family": "Playfair Display", "weight": 700, "style": "italic" }), resolved from Google Fonts at render time.weightis 100..900 in steps
of 100 (default 400),styleisnormaloritalic. Registry ids and.ttffilenames keep
working unchanged, and a typo in a registry id is still a validation error rather than a network
lookup. A family that does not exist fails the render with an error naming it — a missing font is
never silently swapped for another face. - A persistent font cache (Node). Faces resolved by family, and catalog fonts, are copied to
~/.cache/leclap/fonts(override withFVC_FONT_CACHE_DIR), so a repeat render of the same font
needs no network. Entries are written atomically; an unwritable directory only costs the
re-download. isFontRef,FontRefandFontInputare exported from every entry point, andFontRefSchema/
FontInputSchemafrom the Node entry.TemplateValidator.getGeometryWarnings()catches templates that are valid but visually broken,
before anything renders. It lowers each section through the renderer's own text and box filters
(captions, title cards, lower thirds and their badges, global text overlays, authored
drawtext/drawbox) and reports text that runs off the frame or out of the title-safe area,
collides with other text on screen at the same time, is drawn under a band or panel that paints
over it, is too small to read on a phone, has too little contrast against what it sits on, or sits
over footage or an image with no box, outline or shadow. Each finding says what to change.
Findings are advisory: they never entererrorsand never changesuccess. At most 20 are
returned, worst first; when more exist, the last one says how many were left out.- Widths come from the real fonts. The Node entry exports
createBundledFontLoader(bundled fonts
first, then the LeClap asset catalog the renderer uses, 5s timeout per font) and
nodeGeometryWarnings/geometryApproxNote, plus theGeometryWarningandFontLoadertypes.
A finding drawn from an estimate says why:(approx: font unavailable, width estimated),
(approx: {{ variable }} length unknown until render)or(approx: section duration assumed). renderedGeometryWarnings()(Node entry) measures text contrast from rendered pixels, on request.
The render-free check never sees a pixel: text over a picture, under a grade or a look is only
flagged for lacking a box, outline or shadow. This renders the sections that hold text twice — the
second time with every glyph recoloured, so the glyphs are exactly the pixels that change — reads
one frame per piece of text where it rests, and scores it against the pixels around it (lower
quartile, WCAG ratio). It reportstext_low_contrast_renderedbelow 3:1, and over a fixed
backdrop (colour card, picture) it replaces the render-free contrast and over-footage findings
for that text. It costs seconds and needs a native FFmpeg withdrawtext; without one, or when
the render fails, it returns the render-free findings and says why inunavailable. The FFmpeg it
would render with is asked for its filter list first (once per binary), so a build without
libfreetype is named, with what to install, instead of failing every section mid-render.
Changed
AbstractMusic.processreturns the track to mix. It resolves{ rc, musicPath }: the
track it was handed when that already covers the video, else a looped copy in the build
directory. It no longer writes the loop over its input, so a customAbstractMusicmust now
returnmusicPath; the bundled Node, browser and device adapters do.- The browser/WASM backend cannot request a TrueType face from Google, so it now rejects a template
that names a font by family up front, before any section is encoded, with an error listing where.
Ship the fonts you need, or use a bundled font id or a.ttffilename.
Fixed
- In the browser, a segment FFmpeg fails to write no longer passes for a fresh render. ffmpeg.wasm's
in-memory filesystem outlives a render, so an output an earlier render left at the same path
counted as this command's output: a failed segment "succeeded", and a clip FFmpeg couldn't read
rendered as the previous render's clip.FFmpegWasmAdapternow deletes a command's output path
before running it, unless the command also reads that path. - A section that fails to build now fails the whole compile. The build error (an asset or font
that can't be resolved, for instance) used to be logged and swallowed: the section rendered
nothing, the concat skipped it, andcompile()resolvedoutput.mp4without that section.
compile()now resolvesnullon the first failing section, the browser and React Native
entries reject, and the cause is aSectionErrorwhose message names the section —
Section "outro" failed: <cause>. A segment whose render exits non-zero or leaves no output
file fails the compile the same way instead of being dropped from the concat. - Looping a background track shorter than the video no longer rewrites that track. The loop is
written to the build directory and mixed from there. Earlier versions replaced the track in
place — in the CLI'sassets/musics, the MCP media library, the package's bundled tracks, or a
device's staged copy — with a longer MP4/AAC file still named.mp3, so later renders no longer
looped it. Tracks already rewritten are not repaired. - After a failed compile in a long-lived process (e.g.
leclap render --watch), a compile with
music enabled but no track resolved no longer mixes the failed compile's track. - A background track shorter than the video now loops whatever its container. The Node loop
joined copies of the file byte for byte, which only loops a bare MP3 stream: an MP4 container
named.mp3(pop.mp3,point-being.mp3andfuture-bass-energy.mp3in the bundled library,
or a track an earlier version rewrote in place) played once and then fell silent, and an MP3
with embedded cover art (air-prelude.mp3,anxiety.mp3,arcadia.mp3) failed the render,
on device too. Both loops now repeat the track's demuxed audio alone. - A failed FFmpeg command on Node (system FFmpeg or
ffmpeg-static) now reports the lines
FFmpeg failed on, instead of its whole stderr: ten lines of version banner, then a dump of
every input and of the stream mapping, ahead of the line naming the problem. Both adapters
run FFmpeg at-loglevel errorand keep the last 20 lines of what it prints. - Bundled fonts now resolve from a built
dist/(the CLI, the MCP server, any installed consumer),
not only when running from source. - A bundled asset name carrying a path separator or a leading dot is no longer resolved, so a
descriptor font such as../../etc/passwd.ttfcannot read a file outside the library.