How to Create Custom FFmpeg Grade Filters for video-use: A Complete Guide

You can create custom FFmpeg grade filters in video-use by passing raw filter strings via --filter, adding named presets to the PRESETS dictionary in helpers/grade.py, or setting the "grade" field in EDL JSON files to any valid FFmpeg filter chain.

The video-use repository (browser-use/video-use) provides a flexible color grading pipeline that leverages FFmpeg for video processing. While the built-in auto grade provides a subtle clean look, you often need custom color corrections, cinematic LUTs, or specific contrast adjustments. This guide explains how to extend the grading system using your own FFmpeg filter chains.

How the Grade Filter System Works

The colour grading step is implemented in helpers/grade.py, which defines a dictionary of ready-made presets (PRESETS) and the logic that builds filter strings for automatic grading. When the render pipeline runs in helpers/render.py, each EDL entry's grade field is processed by resolve_grade_filter() to generate concrete FFmpeg filter arguments.

The resolve_grade_filter() function accepts four input types:

  • None or empty string: No grading applied (video is copied)
  • "auto": Triggers per-segment auto-grading via auto_grade_for_clip()
  • Preset name: Looks up the filter in the PRESETS dictionary
  • Raw filter string: Any string containing = or , is treated as a literal FFmpeg filter

Three Methods to Create Custom FFmpeg Grade Filters

Method 1: Pass Raw Filters via Command Line

The simplest way to apply a custom grade is using the --filter flag when running grade.py. This bypasses preset lookup and applies the filter verbatim to every segment.

python helpers/grade.py input.mp4 -o output.mp4 \
  --filter "eq=contrast=1.12:brightness=-0.03:saturation=0.95,curves=master='0/0 0.3/0.25 0.7/0.75 1/1'"

The --filter flag overrides any preset configuration and passes the exact FFmpeg filter chain to the processing pipeline.

Method 2: Add a Named Preset to grade.py

For reusable color grades, extend the PRESETS dictionary in helpers/grade.py. After adding your preset, reference it by name in CLI commands or EDL files.

Edit helpers/grade.py and append to the PRESETS dictionary:

PRESETS["my_cinematic"] = (
    "eq=contrast=1.15:brightness=-0.02:saturation=0.90,"
    "colorbalance=rs=0.03:gs=0.00:bs=-0.04:"
    "rm=0.05:gm=0.02:bm=-0.01:"
    "rh=0.07:gh=0.02:bh=-0.05,"
    "curves=master='0/0 0.2/0.18 0.8/0.85 1/1'"
)

Now invoke the preset via command line:

python helpers/grade.py input.mp4 -o output.mp4 --preset my_cinematic

The render script resolves the preset name to the filter string via get_preset().

Method 3: Define Custom Filters in EDL Files

For per-segment control, edit your EDL JSON file directly. Set the "grade" field to either a preset name or a raw filter string.

{
  "grade": "eq=contrast=1.2:saturation=0.95",
  "ranges": [
    {"source": "take1", "start": 0, "end": 12},
    {"source": "take2", "start": 5, "end": 18}
  ],
  "sources": {
    "take1": "videos/take1.mp4",
    "take2": "videos/take2.mp4"
  }
}

When render.py processes this file, resolve_grade_filter() detects the = character and treats the value as a raw FFmpeg filter, passing it unchanged to the per-segment extract_segment() call.

Technical Implementation Details

The auto_grade_for_clip() function produces a subtle "clean-look" filter that never applies creative color shifts. If you need specific aesthetics like teal-orange looks, vintage LUTs, or custom contrast curves, you must supply the full filter chain yourself or extend the preset table.

When apply_grade() processes a segment, it automatically adds -c:v libx264 -pix_fmt yuv420p to the FFmpeg command when a filter is present. This ensures compatibility with the concatenation pipeline that stitches per-segment video files together.

Compatibility and Pipeline Considerations

Because the render pipeline concatenates per-segment video with -vf <filter>, any custom filter you provide must remain compatible with the rest of the pipeline. Specifically:

  • Pixel format: Avoid changing pixel formats unless you also adjust the output options in apply_grade()
  • Filter syntax: Ensure your filter string uses valid FFmpeg syntax with proper escaping for quotes and special characters
  • Performance: Complex multi-stage filters increase processing time proportionally

Summary

  • Three input methods: Use --filter for one-off grades, edit PRESETS in helpers/grade.py for reusable looks, or set the "grade" field in EDL JSON for per-segment control
  • Resolution logic: resolve_grade_filter() in helpers/render.py automatically detects raw filters (containing = or ,) versus preset names
  • Codec handling: The apply_grade() function automatically appends -c:v libx264 -pix_fmt yuv420p when filters are active
  • Auto limitations: The built-in auto_grade_for_clip() provides only subtle corrections; creative grades require custom filter chains

Frequently Asked Questions

What is the difference between "auto" and custom presets?

The "auto" value triggers auto_grade_for_clip(), which calculates per-clip brightness, contrast, and saturation adjustments to create a neutral, clean look. Custom presets in PRESETS allow you to define specific aesthetic filter chains (like cinematic color grades or stylized LUTs) that are applied uniformly without automatic calculation.

Can I combine multiple FFmpeg filters in one grade?

Yes. You can chain multiple FFmpeg filters using commas within your filter string. For example: "eq=contrast=1.15:brightness=-0.02,curves=master='0/0 0.2/0.18 1/1',colorbalance=rs=0.03" combines equality adjustments, curves, and color balance in a single grade pipeline.

Why does my custom filter fail during rendering?

Custom filters usually fail for two reasons: syntax errors in the FFmpeg filter string, or pixel format incompatibilities. Ensure your filter string contains valid FFmpeg syntax (check for proper quote escaping), and verify that your filter does not change the pixel format unexpectedly. The pipeline expects yuv420p output for concatenation compatibility.

Where should I store custom presets for team sharing?

Store custom presets directly in the PRESETS dictionary within helpers/grade.py and commit the changes to your repository fork. This ensures that all team members using the same codebase have access to consistent grade presets. Alternatively, document your filter strings in your project's EDL templates for per-project customization.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →