Codingloop
Loading Clip Forge…
Free Tool

Clip ForgeBranded Video Builder

Build videos from a declarative JSON scene list — captions, photos, cutouts, and video clips with crop/trim/zoom and optional custom stat overlays — then export a deterministic MP4. Entirely in your browser: nothing is ever uploaded to a server. Requires the File System Access API and WebCodecs (Chrome or Edge) to open a project folder and export video — the page itself works everywhere.

How It Works

A template, not an editor

Clip Forge isn't a general-purpose video editor — there's no draggable timeline, no manual keyframing, no frame-by-frame trimming by hand. It exists for a video format you make repeatedly: describe the scene sequence once as JSON, then produce each new video by swapping the text, media, and stats in that same file, and export a deterministic result every time.

Working directory

Picked once via the OS folder picker and remembered across visits. Each immediate subfolder holding a config.jsonshows up as a project in the sidebar — "Save" creates that subfolder for you the first time.

Assets folder

A separate folder for photos/videos, shared by every project — reference a file by its relative path from a scene's src, clip, or backdrop prop (e.g. "clip.mp4"). "+ Add media…" copies a picked file straight in without leaving the browser. Browser permissions reset every session, so a remembered assets folder needs one click on "Reconnect assets folder…" in the sidebar before it's used again — until then, a path is looked up in the project's own folder instead, with no error shown.

Insert project

Because every project draws from the same shared assets folder, "+ Insert project…" can copy another project's entire scene list onto the end of this one — no re-uploading. This is the actual repeat-a-format mechanism.

Handle chrome & safe area

The on-screen @handle chrome and default safe-area insets were originally built around Instagram Reels' own UI, so captions and stats stay clear of its title bar and caption/reply chrome by default — override any inset via meta.safeArea, or turn the chrome off with meta.showHandle: false.

Scenes

One JSON file, four scene types

A project is a single config.json — an ordered list of scenes, each with a component, a duration, and its own props. Edit the JSON, watch the preview update live.

Caption

Background-only scene with a kicker + big line of text. Hooks, statements, plain text beats.

Photo

Full-bleed photo with a slow Ken Burns zoom/pan and an optional caption on top.

Cutout

A background-removed subject floating over an accent-colored glow.

ClipScene

Any video clip in a band, with an optional blurred backdrop still behind it, optional custom stat rows (any icon/label/value), and an optional stopwatch timer.

Deterministic

Every frame rendered standalone — a slow machine only takes longer, never drops a frame

1080 × 1920

Default canvas size (vertical) — configurable per project to any aspect ratio

MP4 / H.264

Encoded client-side via WebCodecs, muxed with mediabunny

30 fps

Fixed export frame rate

Reference

Every field in config.json

Canvas resolution, branding, and the scene list all live in one JSON file, edited live in the tool's built-in editor. Everything below is optional except scenes, component, and dur — anything you skip falls back to a sensible default.

{
  "name": "My Video",                // optional — display name, defaults to "untitled"
  "type": "video",                   // optional — label only, shown in the project list
  "width": 1080,                     // optional — canvas width in px, defaults to 1080
  "height": 1920,                    // optional — canvas height in px, defaults to 1920

  "meta": {                          // optional — branding + look (all fields optional)
    "handle": "@yourhandle",         // shown in the on-screen handle chrome
    "accent": "#7d6fd0",             // accent color used across chrome, progress rule, and glow
    "bg": "#161826",                 // default scene background — every component below can override it
    "statSize": 42,                  // default ClipScene stat value font size in px — each stat item can override it
    "showHandle": true,              // show/hide the @handle + video-number chrome, default true
    "grain": true,                   // film-grain overlay, default true
    "showGuides": false,             // safe-area guide overlays, default false
    "videoNumber": 1,                // number shown next to the handle
    "safeArea": {                    // optional — override any subset of the default safe-area insets (px)
      "left": 66,
      "right": 200,
      "top": 250,
      "bottom": 300
    }
  },

  "scenes": [                        // required — ordered list of scenes, at least one
    {
      "component": "Caption",        // required — Caption | Photo | Cutout | ClipScene
      "dur": 3,                      // required — seconds this scene is on screen
      "props": {
        "kicker": "kicker",          // optional — small label above the line
        "line": "Your line here",    // optional — main text
        "bg": "#1a1030"              // optional — overrides meta.bg for just this scene; every component below accepts this too
      }
    },
    {
      "component": "Photo",
      "dur": 4,
      "props": {
        "src": "assets/photo.jpg",   // required — path to the image inside the assets folder
        "from": 1.2,                 // optional — starting zoom scale, default 1.2
        "to": 1.05,                  // optional — ending zoom scale, default 1.05
        "panFrom": 0,                // optional — starting vertical pan in px, default 0
        "panTo": 0,                  // optional — ending vertical pan in px, default 0
        "dim": 0,                    // optional — darken amount 0-1, default 0
        "kicker": "kicker",          // optional
        "line": "Caption over photo" // optional
      }
    },
    {
      "component": "Cutout",
      "dur": 4,
      "props": {
        "src": "assets/cutout.png",  // required — background-removed subject image
        "width": 872,                // optional — rendered width in px, default 872
        "top": 110,                  // optional — top offset in px, default 110
        "glowTop": 690,              // optional — vertical center of the accent glow in px, default 690
        "kicker": "kicker",          // optional
        "line": "Caption over cutout" // optional
      }
    },
    {
      "component": "ClipScene",
      "dur": 6,
      "props": {
        "clip": "assets/clip.mp4",       // required — the video clip
        "backdrop": "assets/still.jpg",  // optional — blurred still shown behind the video band
        "bandTop": 496,                  // optional — video band top offset in px, default 496
        "bandHeight": 640,               // optional — video band height in px, default 640
        "clipStart": 0,                  // optional — trim: start second in the source file, default 0
        "clipEnd": 120,                  // optional — trim: end second in the source file
        "cropLeft": 0,                   // optional — crop % off the left edge, default 0
        "cropRight": 0,                  // optional — crop % off the right edge, default 0
        "cropTop": 0,                    // optional — crop % off the top edge, default 0
        "cropBottom": 0,                 // optional — crop % off the bottom edge, default 0
        "zoom": 8,                       // optional — zoom-in amount %, default 0 (off)
        "zoomDuration": 6,               // optional — seconds to ramp the zoom in (Ken Burns); omit for a static zoom
        "showTimer": true,               // optional — show the stopwatch-style timer counting up to timeTaken, default false
        "timeTaken": "3h 40m",           // optional — the timer's target duration ("3h 40m", "45m", "1:02:03" all parse)
        "stats": [                       // optional — arbitrary icon/label/value rows, nothing is a fixed field
          { "icon": "printer", "label": "Printer", "value": "Bambu X1C" },
          { "icon": "filament", "label": "Material", "value": "PLA" },
          { "icon": "clock", "label": "Time", "value": "3h 40m", "newLine": true, "size": 32 },
          { "icon": "scale", "label": "Weight", "value": "42g", "size": 32, "color": "#9184d9" }
        ]
      }
    }
  ]
}

The // comments above are for reference only, since config.json must be valid JSON — the copy button strips them automatically, so what lands on your clipboard is ready to paste in as-is.

A stats item's icon is optional and picks from a small hand-drawn set — printer, filament, clock, scale, tag, flag, star, camera — no icon font or CDN, so it costs nothing extra to load. label and value are free text; items pack onto the same line until one sets newLine: true, which starts a fresh row. size (px, scales the icon and label with it) and color are both optional per item — size falls back to meta.statSize (itself defaulting to 42) when unset.

FAQ

Common questions

Have feedback or found a bug?

Let us know — we read every message.