A minimal, self-hostable audio/video editor built around a freeform canvas instead of a fixed timeline. Clips are cards you drop anywhere on an infinite, pannable/zoomable surface; nudge them edge-to-edge and they snap into chains that play and export as one continuous piece. There are no locked, linear tracks — lay clips out however you think, and let adjacency be the timeline.
Everything happens on that one blank canvas with a single top toolbar: upload clips, arrange and trim them into chains, play the result gaplessly, and export it as one file.
No build step: the frontend is plain ES modules served by a FastAPI backend. The browser only edits an EDL (edit decision list); all heavy media work (probing, waveform/filmstrip generation, proxy transcoding, compositing) happens server-side where ffmpeg lives.
Status: v0.1.0 — the editor core plus an async job framework (exports run as background jobs with live progress: SSE push, poll fallback).
- Freeform canvas. Clips live anywhere on a pannable, zoomable canvas. Scroll / two-finger drag (or drag empty space) to pan; Ctrl/⌘+wheel or trackpad pinch to zoom toward the cursor; minimap slider + Fit too. One-finger drag pans on touch.
- Snap into chains. Drag a clip near another's edge and it snaps to attach; attached clips form a chain that travels together (grabbing a mid-clip carries its right-tail). A chain is what plays / exports.
- Mode-locked. The first clip sets the project to video-only or audio-only (toolbar pill shows the mode); a video clip's audio is tied to it.
- Visual clips. Audio clips show a normalised waveform; video clips show a
filmstrip with the waveform beneath. Peaks + thumbnails are generated by
ffmpegon upload and cached on disk. - Edge-trim with re-expand. Drag a clip's edges to shorten it and back out to its original length; the chain's tail follows so it stays attached. A trim view (double-click a clip) gives precise in/out editing against the whole source with a filmstrip/waveform track and region playback.
- Gapless, media-clock playback. Select a clip → play its chain to the end. Two media elements ping-pong (the next clip is pre-decoded on the hidden one), and the playhead is driven by the playing element's own clock — so no load-time jump, no A/V drift, no black frames at cuts.
- Proxy preview (browser-agnostic, VPN-friendly). Playback streams a small,
universally-decodable H.264 proxy (480p in the corner monitor by default,
generated by
ffmpegon upload and cached) rather than the heavy original — so preview works in any browser and over limited bandwidth. The monitor has player controls (play/stop, scrubber, buffered indicator) and is resizable; a Fullscreen button plays the original quality. Export always uses the originals. - Light / dark theme, applied before first paint and remembered across sessions.
- Export dialog. Server-side
ffmpegcomposites the selected chain into one file — video as mp4 / webm / gif, audio as m4a / mp3 / wav, with resolution and quality options — as an async job with a progress tray, then downloads the result. Undo/redo, copy/paste, and a project switcher round out the editing basics. - Multi-user ready. Behind a forward-auth reverse proxy (e.g. Caddy + Authentik) each signed-in user gets a fully isolated workspace — own projects, media, and jobs, enforced server-side. Without a proxy it runs in single-user mode.
docker compose up --build # → http://127.0.0.1:3011Data (uploads, renders, SQLite) lives in the clipknit-data volume.
The default compose runs single-user (CLIPKNIT_ALLOW_NO_AUTH=1) — no
reverse proxy or identity provider needed; everything belongs to one local
user. For multi-user, put a forward-auth proxy in front that strips
client-supplied X-Authentik-* headers and injects the real X-Authentik-Uid,
and unset CLIPKNIT_ALLOW_NO_AUTH — with neither the header nor the opt-in,
requests are rejected (fail-closed). Local deployment tweaks belong in a
docker-compose.override.yml (gitignored).
No build step — edit web/ and app/ directly (the Docker image bakes web/
in, so rebuild to see frontend changes in the container).
python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
.venv/bin/python -m pytest # backend suite (ffmpeg/ffprobe are mocked)
npm test # frontend geometry suite (node --test, Node 18+, no deps)| Var | Default | Notes |
|---|---|---|
CLIPKNIT_DATA_DIR |
/data |
uploads, renders, sqlite |
CLIPKNIT_MAX_UPLOAD_BYTES |
2147483648 |
0 = unlimited |
CLIPKNIT_FFMPEG / CLIPKNIT_FFPROBE |
ffmpeg / ffprobe |
point at a jellyfin-ffmpeg build for NVENC |
CLIPKNIT_VIDEO_CODEC |
libx264 |
set h264_nvenc on the GPU deploy |
CLIPKNIT_RENDER_WIDTH / _HEIGHT / _FPS / _SR |
1280 / 720 / 30 / 44100 |
render target |
CLIPKNIT_PEAKS_PER_SEC / CLIPKNIT_PCM_SR |
100 / 8000 |
waveform resolution |
CLIPKNIT_STRIP_H / _STRIP_FPS / _STRIP_MAX_FRAMES |
60 / 1.0 / 240 |
filmstrip thumbnails |
The default image bundles CPU ffmpeg. For hardware export, run under the nvidia
runtime with NVIDIA_DRIVER_CAPABILITIES including video, mount/point
CLIPKNIT_FFMPEG at an NVENC-capable ffmpeg, and set
CLIPKNIT_VIDEO_CODEC=h264_nvenc.
POST /api/media?project_id=(multipart upload) ·GET /api/media·DELETE …GET /api/media/{id}/file(Range) ·/proxy/{h}·/peaks·/strip.json·/strip.jpg(derived assets, lazily generated)GET/POST/PUT/DELETE /api/projects[/{id}]— v2 EDL ({version, mode, clips}) stored as JSONPOST /api/projects/{id}/clips— append a clip, enforcing the mode lock (409 on mismatch)POST /api/jobs({type:"render", project_id, params}) ·GET /api/jobs/{id}— export runs as an async jobGET /api/jobs/events— one SSE stream of all your jobs' progress (push; the client falls back to polling)- See OVERVIEW.md §"API surface" for the full table (sessions, sweep,
/api/me, health)
0BSD.