Install
$ agentstack add skill-damionrashford-media-os-ffmpeg-geq-expr ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
ffmpeg-geq-expr — procedural expression filters
Context: $ARGUMENTS
Power-user creative filters where you hand-write a math expression and ffmpeg evaluates it per-pixel (geq), per-sample (aeval), per-pair (lut2), or per-metric (drawgraph). Grammar is the same everywhere: see references/expressions.md.
Quick start
- Custom per-pixel effect: → Step 1 → pick
geq - Custom per-sample audio effect: → Step 1 → pick
aeval - Blend/combine two videos pixel-by-pixel with math: → Step 1 → pick
lut2 - Real-time metric graph burned into video: → Step 1 → pick
drawgraph - SVG overlay driven by frame metadata: → Step 1 → pick
drawvg - Light-on-dark persistence / phosphor trails: → Step 1 → pick
lagfun - Feedback-loop picture-in-picture: → Step 1 → pick
feedback - Shuffle frame order: → Step 1 → pick
random
When to use
- You need a per-pixel/per-sample effect that no named filter ships with (gradients, plasma, custom noise, chromatic aberration, vignettes).
- You want to wire filter metadata (
signalstats,ebur128,blockdetect) into a visible QC graph. - You want to combine two aligned inputs with arbitrary math, not
blend's fixed modes. - You want a one-off creative look without writing a C filter.
Do NOT use for things with a dedicated filter — geq is typically 10–100× slower than a native filter. If vignette, lut3d, colorchannelmixer, curves, etc. can do it, use those.
Step 1 — Pick the right filter
| Need | Filter | Domain | |---|---|---| | Per-pixel math on one video | geq | spatial (X,Y) + temporal (T,N) | | Per-sample math on audio | aeval | per-sample per-channel | | Two-video pixel math | lut2 | pairs of samples (x, y) from inputs | | Graph metadata values | drawgraph | per-frame scalar from metadata keys | | SVG / VGS vector overlay | drawvg | per-frame VGS script | | Frame-order shuffle | random | temporal only | | Feedback picture-in-picture | feedback | spatial box + time | | Dark-decay afterimage | lagfun | per-pixel with decay |
Step 2 — Write the expression
Grammar highlights (full table in references/expressions.md):
- Built-in constants:
PI,E,PHI,TAU. - Functions:
sin,cos,tan,asin,acos,atan,atan2,exp,log,sqrt,hypot(a,b),pow(a,b)ora^b,mod(a,b),abs,floor,ceil,round,trunc,clip(v,a,b),between(v,a,b),if(c,t,e),ifnot(c,e),gt(a,b),lt(a,b),gte,lte,eq(a,b),not(x),random(seed),st(N,v)store,ld(N)load (26 slots, N=0..25),print(v)to stderr. - Operators:
+ - * / %,^power,&& || !logic,? :ternary is NOT supported (useif(c,t,e)).
geq per-plane YUV:
geq=lum='X+T*10':cb=128:cr=128 # moving luminance ramp, neutral chroma
geq=lum='255*(X/W)':cb='128+64*sin(T)':cr='128+64*cos(T)'
geq RGB (requires planar RGB pixfmt):
format=gbrp,geq=r='255*X/W':g='255*Y/H':b=0 # diagonal RG gradient
format=gbrp,geq=r='b(X,Y)':g=g:b='r(X,Y)' # BGR channel swap
Vignette via geq (hand-rolled):
format=gbrp,geq=r='r(X,Y)*clip(1-pow((X-W/2)/(W/2),2)-pow((Y-H/2)/(H/2),2),0,1)':g='g(X,Y)*clip(1-pow((X-W/2)/(W/2),2)-pow((Y-H/2)/(H/2),2),0,1)':b='b(X,Y)*clip(1-pow((X-W/2)/(W/2),2)-pow((Y-H/2)/(H/2),2),0,1)'
aeval (per-sample):
aeval=val(0)*0.5|val(1)*0.5:channel_layout=stereo # halve volume (note | between exprs)
aeval=val(ch)*sin(2*PI*5*t):c=same # 5 Hz tremolo
aevalsrc (synth):
aevalsrc='sin(2*PI*440*t)':channel_layout=mono:sample_rate=48000:duration=2
lut2 (two inputs, same size/pixfmt):
lut2=c0='(x+y)/2':c1='(x+y)/2':c2='(x+y)/2' # pixel average blend
drawgraph (metadata chart):
signalstats,drawgraph=m1=lavfi.signalstats.YAVG:fg1=0xff00ff:min=0:max=255:size=1280x200
drawvg (SVG/VGS overlay):
drawvg=cfg=''
random (frame order):
random=frames=30:seed=42
feedback (recursive PiP):
feedback=x=10:y=10:w=100:h=100
lagfun (dark decay / afterimage):
lagfun=decay=0.9
Step 3 — Run and debug
Always test on 2 seconds first:
ffmpeg -i in.mp4 -t 2 -vf "" -y out_test.mp4
See parse errors: -loglevel debug prints tokens as they parse. A silent output or black frames without stderr output almost always means a typo or an operator ffmpeg's parser rejected.
Drop print() into any expression to dump a value per evaluation to stderr:
geq=lum='print(T)':cb=128:cr=128
Use the helper:
uv run ${CLAUDE_SKILL_DIR}/scripts/expr.py geq --input in.mp4 --output out.mp4 --lum 'X+T*10'
Available scripts
scripts/expr.py— subcommands:geq,aeval,lut2,drawgraph,feedback,lagfun. All support--dry-runand--verbose. Autodetects RGB vs YUV forgeqbased on which flags you pass.
Reference docs
- Read [
references/expressions.md](references/expressions.md) for the full grammar, geq constant table, store/load slot usage, 50+ worked recipes, and thedrawgraphmetric catalog.
Gotchas
geqcoordinates:X/Yare 0-based pixel coordinates,W/Hare plane dimensions,SW/SHare the horizontal/vertical subsampling factors (useSW/SHin chroma expressions on 4:2:0, where chroma planes are half-size).Tis time in seconds (float).Nis the frame number.p(x,y)inside a chroma (cb/cr) expression returns a chroma sample, not luma. If you want luma inside a chroma expression uselum(X,Y). Likewiselum(),cb(),cr(),alpha(),r(),g(),b()all exist.clip(v,a,b)clamps;st(N,v)stores in slotN(0..25),ld(N)reads. Use these when an expression appears multiple times.geqon RGB input silently operates on packed pixels unless you insertformat=gbrpfirst. Always prependformat=gbrpbefore a color-channelgeq.- Expressions are compiled but evaluated per pixel, per frame. Complex expressions can drop processing to sub-1 fps. Cache common sub-expressions with
st()/ld(). aevalevaluates per audio sample and is slow. For synth (not a real-time effect on a real stream), useaevalsrc— much faster because there's no input rate.aevalvariables:val(i)is input channeli's current sample,chis output channel index,nb_in_channels,nb_out_channels,ssample rate,ttime,nsample number.lut2: expression usesx(input 0 sample) andy(input 1 sample). Both inputs must have the same dimensions AND pixel format. Insertscale=+format=before the join.drawgraph: background defaults to transparent. If you want it on top of video, pipe throughoverlay. To output a standalone graph, addbg=blackor use thecolorsource as a backdrop.drawvgis new (post-FFmpeg 7.x). Checkffmpeg -filters 2>&1 | grep drawvgbefore relying on it.feedbackrequires all fourx,y,w,h— withoutw/hit silently fails.- When an expression has a parse error, ffmpeg commonly silently outputs black frames or stops without a loud error. Run with
-loglevel debugand search the output forError when evaluating. PI/E/PHI/TAUare built-in; don't redeclare them asst().- Comma
,separates filter options in ffmpeg syntax. Inside an expression, use\,(escaped) or wrap the whole expression in single quotes. - Semicolon
;separates filter-graph stages; use|to separate per-channel expressions insideaeval. - Always test expressions with
-t 2on a short clip first, then scale up.
Examples
Vertical gradient (RGB)
ffmpeg -f lavfi -i color=black:s=640x360:d=2 -vf "format=gbrp,geq=r='255*Y/H':g=0:b='255-255*Y/H'" -y grad.mp4
Moving plasma
ffmpeg -f lavfi -i color=black:s=640x360:d=3 \
-vf "format=gbrp,geq=r='128+127*sin(X/20+T)':g='128+127*sin(Y/20+T*1.3)':b='128+127*sin((X+Y)/20+T*0.7)'" \
-y plasma.mp4
Chromatic aberration
ffmpeg -i in.mp4 -vf "format=gbrp,geq=r='r(X+2,Y)':g='g(X,Y)':b='b(X-2,Y)'" -y ca.mp4
YAVG graph burn-in
ffmpeg -i in.mp4 -vf "signalstats,drawgraph=m1=lavfi.signalstats.YAVG:fg1=0xffff00:min=0:max=255:size=1920x120,format=yuv420p" -c:v libx264 -crf 18 -y graph.mp4
Lagfun phosphor trail
ffmpeg -i in.mp4 -vf "lagfun=decay=0.92" -y trail.mp4
Two-video lut2 average blend
ffmpeg -i a.mp4 -i b.mp4 -filter_complex "[0:v]scale=1280:720,format=yuv420p[a];[1:v]scale=1280:720,format=yuv420p[b];[a][b]lut2=c0='(x+y)/2':c1='(x+y)/2':c2='(x+y)/2'" -y blend.mp4
Troubleshooting
Black frames, no obvious error
Cause: expression parse error silently produced all-zero output. Solution: add -loglevel debug, search stderr for Error when evaluating, check for unescaped commas in expression, or wrong variable name (e.g. x inside geq should be X).
geq on RGB but colors ignored / operating on packed bytes
Cause: input is packed RGB (rgb24, bgr24), not planar. Solution: prepend format=gbrp before geq.
lut2 error: inputs dimensions differ
Cause: two inputs have different resolutions or pixel formats. Solution: prepend scale=W:H,format=yuv420p on both inputs before joining.
geq runs at 0.3 fps
Cause: expression is complex, recomputed per-pixel. Solution: cache repeated sub-expressions with st(0,...) then reuse with ld(0). Also: consider if a native filter (vignette, colorchannelmixer, curves) can do the same thing.
drawgraph shows nothing
Cause: metadata keys are wrong, or the producer filter wasn't upstream. Solution: signalstats → drawgraph=m1=lavfi.signalstats.YAVG. Run ffprobe -show_frames to confirm the metadata keys.
aevalsrc produces clipped audio
Cause: expression output exceeds ±1.0. Solution: scale the expression: sin(2*PI*440*t)*0.5.
feedback silently does nothing
Cause: missing one of x, y, w, h. Solution: supply all four. Also check coords are inside the frame.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: damionrashford
- Source: damionrashford/media-os
- License: MIT
- Homepage: https://damionrashford.github.io/media-os/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.