# FableCut — browser video editor, drivable by Claude Code A production-style non-linear video editor (Premiere-style) that runs in the browser. An AI agent edits videos by **editing `project.json`** (or calling the REST API / MCP tools) — the open browser UI live-reloads within ~150 ms via SSE. No build step, no npm dependencies. **This file is the master manual.** Any model pointed at this document (or at the `fablecut_docs` MCP tool, which returns it) has everything needed to fully drive the editor. ## MCP connection (preferred — works from any session, any directory) Register the MCP server once at user scope as `fablecut`: `claude mcp add -s user fablecut -- npx -y fablecut mcp` (from npm, no path needed), or from a clone `claude mcp add -s user fablecut -- node "/fablecut/mcp-server.js"`. Every Claude Code session then has these tools: - `fablecut_status` — auto-starts the editor server, returns URL + project summary. Call first. - `fablecut_docs` — returns this document (`section: "…"` returns only matching `## ` sections). - `fablecut_get_project` / `fablecut_set_project` — read / replace the timeline JSON. `fablecut_get_project {compact:true}` returns a one-line-per-clip summary instead. - `fablecut_patch_project` — apply targeted ops (add/update/remove clip/media, set project fields, add and open sequences) without round-tripping the document. **Prefer this for edits.** - `fablecut_import_media` — copy a local file (or download an `https://` URL) into `./media/` and register it. The stored `src` is always `/media/…`. - `fablecut_analyze_reference` — turn a reference video into an edit blueprint (shots, beats, BPM, energy, drop) + extract its music. See "Remake a reference video". - `fablecut_frame` — **look at** a moment and get back an actual image. `{path, t}` returns one frame of a source clip; `{path, frames}` returns a contact sheet across it; with no `path`, `{time}` renders the composed timeline. See "Look at the footage" below. - `fablecut_encode_profiles` — list the **delivery presets** (YouTube, Shorts, Reels, TikTok, X, LinkedIn, Master, Broadcast, Web, Archive, saved ones) and the raw ffmpeg **encoding profiles** from `encoding-profiles.json`. `{preset:"youtube"}` returns one preset's spec. See "Export" below. - `fablecut_normalize_audio` — measure clips (BS.1770 LUFS or sample peak, via ffmpeg) and set their clip gain to a loudness target. See "Audio mix" below. - `fablecut_auto_duck` — find where the voice tracks speak and write `duck` keyframes that dip the music under it. See "Audio mix" below. - `fablecut_frame` — **see** a moment. `{path, t}` → one JPEG frame of a source clip; `{path, frames:12}` → a contact sheet across it; `{time}` → the composed timeline, graded, as export renders it. Returns a real image, not numbers. See "Look at the footage" below. - `fablecut_denoise` — reduce background noise on audio clips (renders a cleaned copy of the file and switches the clip's stems to it). See "Audio mix" below. - `fablecut_silence` — find the pauses in speech and, with `cut:true`, cut them out (extract on the targeted tracks, stems in step). See "Audio mix" below. - `fablecut_beats` — find a music clip's tempo, beat and drop; mark the beats, snap the cuts to them, or re-cut a track's clips on the beat. See "Audio mix" below. - `fablecut_reframe` — reframe for another shape (9:16, 1:1, 4:5): a copy of the sequence on that canvas whose shots follow the subject. See "Sequences" below. - `fablecut_highlights` — the best moments of long footage cut into a short edit, in a new sequence. See "Sequences" below. - `fablecut_transcribe` — turn speech into words with times (Whisper, in the browser), read them on the timeline, find a phrase and cut it out. See "Transcripts and text-based editing" below. - `fablecut_captions` — caption clips from the transcript, timed word by word (subtitle, karaoke, highlight, word pop); export or import SRT / WebVTT. See "Captions" below. - `fablecut_export` — render the timeline to a file in `exports/` with the editor's own Fast export (open tab, or headless Chrome / Edge). See "Export" below. - `fablecut_scopes` — measure the graded picture at a moment (levels, clipping, colour cast), as the editor's scopes see it; also what a grade layer or a clip's masks select. See "Color" and "Masks" below. - `fablecut_track` — motion tracking: follow a region of a video clip, store it as a track, and make a mask or another clip follow it. See "Tracking" below. - `fablecut_proxy` — make, check or delete proxies: light copies of heavy footage the editor plays while editing (export always reads the originals). See "Media management" below. - `fablecut_projects` — list, open, create (from a canvas or a saved template), duplicate, rename, annotate, archive and delete projects. `./project.json` is always the open one. See "Projects" below. - `fablecut_versions` — a project's saved versions: save one before big edits, compare a version with now, restore one (never destructive), mark the final cut. See "Versions" below. ### Token-efficient editing (important for agents) Editing via full get→modify→set costs thousands of tokens per change. Cheaper: 1. **Plan** from `fablecut_get_project {compact:true}` (≈10× smaller than the JSON) and `fablecut_status` — fetch the full JSON only to inspect exact keyframes. 2. **Edit** with `fablecut_patch_project` ops — send only what changes, e.g. `{ops:[{op:"updateClip", id:"c_v2", set:{props:{filterPreset:"noir"}}}]}`. It re-reads the latest document internally, so it is merge-safe by design (no CONFLICT dance) and never destroys concurrent UI tweaks. To also be sure the clips you touch haven't changed since you looked, pass `baseRevision` (the `revision` you planned from — `fablecut_get_project`, `fablecut_status` or the last "Patched (revision N)" result): if the project has moved past it, the patch is refused with "CONFLICT — not saved" and the current revision. Re-check the clips, then retry with that revision. 3. **Docs**: request `fablecut_docs {section:"schema"}` (or "Recipes", "Remake", …) instead of the whole manual; skip it entirely if the schema is already in context. 4. **Media questions** (duration, resolution, fps, codec, audio channels, file size, how often it is used): read them from the registered media entries or the compact view's MEDIA lines — don't shell out to ffprobe; the editor probes every file and writes them back (see "Media management"). For what the footage *looks like*, use `fablecut_frame` — it is far cheaper than reading a file, and far more reliable than inferring from a number. 5. Batch related changes into ONE patch call (ops apply in order, one revision bump). 6. **Cut and trim with the edit ops** (next section) rather than recomputing `start` / `in` / `duration` by hand — they keep linked stems in sync. ### Timeline edit ops (`fablecut_patch_project`) The editor's own split / ripple / trim code (`edit-ops.js`), run on the document — so linked audio stems ride along, edits land on the **targeted** tracks (`untargetedTracks`, see Semantics), and locked clips stay put, exactly as with the keyboard shortcuts. Times are seconds. | op | does | |---|---| | `{op:"split", at, ids?}` | cut at `at` — the named clips (and their stems), or every targeted track | | `{op:"rippleDelete", ids}` | remove clips; later clips on those tracks close the hole | | `{op:"closeGap", at}` | close the gap under `at` on every targeted track | | `{op:"lift" \| "extract", from?, to?}` | remove a range (default: `inPoint`→`outPoint`, which then clear); extract also closes it | | `{op:"insert" \| "overwrite", mediaId, at, in?, duration?}` | three-point edit: place `in`→`in+duration` of the media at `at` (default: the rest of the media). Insert pushes later clips right; overwrite replaces what is under it. A video brings one linked audio stem per channel. | | `{op:"rippleTrim", id, side:"in"\|"out", delta}` | move a head / tail; everything after follows | | `{op:"roll", id, side, delta}` | move the cut between two clips; nothing else moves | | `{op:"slip", id, delta}` · `{op:"slide", id, delta}` | new source window in place · move a clip between its neighbors | | `{op:"crossfade", ids? \| at, duration?}` | constant-power audio crossfade at the cuts touching `ids`, or the cut nearest `at` (handles borrowed from both sides) | Trims are clamped to the source media, the free room and `MIN_DUR`; the result note shows the applied delta (`… (asked +5, clamped)`). Any edit op takes `tracks:[…]` to target lanes for that op only, and `force:true` to edit locked material (only when the user asked). A refused op (nothing under `at`, a locked clip, a media window past the file's end) aborts the whole patch unsaved. `setProject` also sets `inPoint` / `outPoint` (`null` clears). Example — cut the 2 s from 10 s to 12 s out of the edit, then crossfade the new cut: `{ops:[{op:"extract", from:10, to:12}, {op:"crossfade", at:10, duration:0.5}]}` **`fablecut_set_project` is conflict-checked.** The MCP server remembers the `revision` from the most recent `fablecut_get_project` call. If `project.json` has been written by anyone else since that read (e.g. the user dragged a clip in the UI), `fablecut_set_project` refuses with a "CONFLICT — not saved" error instead of overwriting. Protocol: 1. `fablecut_get_project` → read the document and note its `revision`. 2. Apply your edits in memory, bump `revision`. 3. `fablecut_set_project` → if it succeeds you're done. 4. **On conflict**: call `fablecut_get_project` again to get the latest document, re-apply your intended changes on top of it, bump `revision`, and call `fablecut_set_project` again. Pass `force: true` to `fablecut_set_project` only when the user explicitly asks to overwrite conflicting changes. `fablecut_import_media` only appends a new media entry and always merges safely — no conflict check needed. For Claude Desktop, add to its MCP config: `{"mcpServers":{"fablecut":{"command":"npx","args":["-y","fablecut","mcp"]}}}` (or `"command":"node","args":["/fablecut/mcp-server.js"]` from a clone) Direct file editing of `project.json` (below) works too and is equivalent. Installing as a Claude Code plugin (`/plugin marketplace add ronak-create/FableCut`, then `/plugin install fablecut@fablecut`) does the registration for you. ### Where the files are `project.json`, `projects/`, `media/`, `exports/`, `analysis/` and `library/` normally sit in the repo next to `server.js`; installed from npm (`npx fablecut`) they sit in `~/FableCut`. Set **`FABLECUT_DATA_DIR`** to move all six somewhere else; the code and the static app files stay in the install directory either way. The plugin sets this so a plugin update can replace the install directory without touching anyone's timeline or footage. **Don't assume `project.json` is beside `mcp-server.js`** — call `fablecut_status`, which reports the real paths. Tests (and nothing else) may set **`FABLECUT_NO_FS_WATCH=1`** to skip `fs.watch`. On Windows, libuv can abort the process when a file is created under a temp data dir. Production leaves watching on so the UI live-reloads. Tests may also set **`FABLECUT_NO_AUTOSTART=1`** so `ensureUIServer()` reports a missing editor server instead of spawning one — the auto-start is detached, so a test that triggers it would leave a `server.js` running after it finishes. ## Run ``` node server.js # → http://localhost:7777 (from npm: npx -y fablecut) ``` Files: `index.html` + `style.css` + `app.js` (editor UI), `server.js` (API + hosting), `project.json` (the timeline — THE file to edit), `media/` (project footage), `library/` (default asset library, see below). ## How Claude Code edits a video 1. Ensure the server is running (background: `node server.js`, or `fablecut_status`). 2. Put source files in `./media/` (copy them in, import via the UI / **+ URL**, or `fablecut_import_media` with a local path or HTTPS URL). 3. Read `project.json`, modify `media` / `clips`, **increment `revision`**, write it back. 4. The browser UI (if open) reloads instantly. The user previews/exports from the UI. Rules: - **Prefer `fablecut_set_project`** over direct file writes — it detects conflicts automatically (see the MCP section above). If you do write `project.json` directly, read it **immediately** before writing (never write from a stale read: if the user tweaked something in the UI between your read and write, that write destroys their changes). The UI detects external changes by revision comparison, so a write that does not bump `revision` is invisible to it. - Make each edit a single atomic write (read → modify → write once), and bump `revision` (integer). Partial multi-step edits can be picked up half-finished. - New media entries may omit `duration` — the browser probes it and writes it back. If you need the duration yourself, re-read `project.json` after a second or two, or probe with ffprobe. - Don't edit `project.json` while the UI may be mid-drag — the UI defers external reloads during gestures, then picks up the next change. ## The asset library (`./library/`) — default media Reusable assets, visible in the editor's left-panel tabs and never copied: | Folder | Editor tab | Purpose | | ----------- | ------------ | ------- | | `library/sfx/` | **Sound FX** | whooshes, impacts, risers, UI clicks | | `library/elements/` | **Elements** | overlay art: alpha PNGs, light leaks, textures, stickers | | `library/svg/` | **SVG** | animated vector graphics **you author** (convention below) | | `library/fonts/` | font editor | `.ttf/.otf/.woff/.woff2`, auto-registered, family name = file name; a variable font draws every `weight` | | `library/presets/` | Inspector → Motion → Presets | saved animation presets, one `.json` each (see "Presets and templates") | | `library/templates/`| **Templates** | saved templates, one `.json` each, listed after the built-in ones | - List via `GET /api/library?dir=sfx|elements|svg|fonts|presets|templates` (recursive; subfolders OK). - To use one in the timeline, add a media entry whose `src` is its library path, e.g. `{ "id":"m_x", "name":"whoosh.mp3", "kind":"audio", "src":"/library/sfx/whoosh.mp3" }` — then reference it from clips like any other media. - Dropping files into these folders live-refreshes the open UI. ## Authoring animated SVGs (the `svg` clip kind) You can create your own vector animations/overlays: write an `.svg` file into `library/svg/` (or `media/`), register it as media with `"kind": "svg"`, and place it on a video track. The compositor renders it frame-accurately, driven by the clip's local time (preview and export). **Conventions (required for time-driven animation):** 1. Root `` must carry `width` and `height` attributes (or a `viewBox`). 2. Animate with **CSS `@keyframes` inside a `