diff --git a/README.md b/README.md index f052647..f040e3a 100644 --- a/README.md +++ b/README.md @@ -38,19 +38,21 @@ frames it's editing. Nothing is re-encoded until you export. Plenty of tools bolt a chatbot onto a video editor. Kerf inverts that: the editor **is** an API, exposed twice over one shared project — as the GUI you click, and as [Model Context Protocol](https://modelcontextprotocol.io) tools an agent calls. So the -agent isn't scripting a black box; it drives the identical engine, and every edit it -makes shows up **live** in your timeline for you to review. +agent isn't scripting a black box; it drives the identical engine — and its edits are +**staged for review**, so they never move the cut you are looking at. - 🎬 **The agent has eyes.** `get_frame`, `skim_asset` (a contact-sheet montage for finding the good parts), and `preview_timeline` (the composited cut at a given time) return real images the model *sees* — so it can find the right moment and confirm the cut, not guess from metadata. - 🧠 **Same engine, same project.** The MCP server and the webview hold the *same* - `Project`. When the agent trims a clip, your GUI re-renders it instantly. -- 🤝 **You stay in control.** Work is handed back and forth through a **task queue**: you - enqueue plain-language tasks, the agent claims one, does the edits, and marks it *ready* - for you to apply or dismiss. Kerf never edits on its own, and every change is a - revertible entry in the edit history. + `Project` behind one lock — no import/export round-trip, no second copy of the truth, + and the agent previews its own work through the exact graph that renders your export. +- 🤝 **You stay in control.** Agent edits **stage** — they build a *proposal*, not your + cut. The review card shows a real diff (added / cut / moved / retrimmed), you can + *watch* the proposed timeline play, and Apply lands it as one revertible revision. + Work is handed back through a persisted **task queue**, so an agent is safe to leave + running on someone's edit. - ✂️ **Truly non-destructive.** Everything is an EDL over source ranges. Cuts, effects, and keyframes are just data until you export a single `filter_complex`. - 📦 **Runs without FFmpeg dev libraries.** The engine drives the `ffmpeg`/`ffprobe` @@ -63,16 +65,20 @@ makes shows up **live** in your timeline for you to review. | | | | --- | --- | -| **Multi-track NLE** | Bespoke timeline: video/audio/text tracks, free clip positioning with gaps, drag-to-move across tracks, edge-drag trim, razor split, ripple delete, snapping to edges / playhead / **beats**. | -| **Audible playback** | Real Web-Audio playback with J/K/L shuttle + scrub; volume, fades, speed and reverse are auralized and the playhead follows the audio clock. | -| **Analysis** | FFmpeg `silencedetect`, scene detection, audio energy/**beat** grid, and waveforms — all CLI-driven. Optional local **Whisper** transcription. | -| **Effects & color** | Per-clip video (`blur`/`sharpen`/`hue`/`negate`/`vignette`/`chromakey`) and audio (`highpass`/`lowpass`/`EQ`/`compressor`/`gate`) chains, plus transform + color grade. | +| **Multi-track NLE** | Bespoke timeline: video/audio/text tracks, free clip positioning with gaps, drag-to-move across tracks, edge-drag trim, razor split, ripple delete, markers, mute/solo/lock, snapping to edges / playhead / **beats**. | +| **Real playback** | Not a slideshow — one long-lived FFmpeg streams the **composited** cut (every track, effect, keyframe and overlay) paced against the audio clock, with J/K/L shuttle and audible Web-Audio scrub. | +| **Delivery frames** | 16:9 / 9:16 / 1:1 / 4:5 is a property of the **project**, so preview, scrub and export all render the same frame — with platform **safe-area guides** and cover-crop instead of letterboxing. | +| **Analysis & speech** | Silence and scene detection, an onset/**tempo** grid, EBU R128 loudness and speech-vs-music from one decode — plus **transcription in every build**, via FFmpeg's `whisper` filter or in-process whisper.cpp (models fetched on first use). | +| **Cut to the beat** | The tempo grid is drawn on the ruler and snapped to while dragging, and `snap_to_beats` ripples a whole track's cuts onto it, retrimming each clip at its **outgoing** edge. | +| **Effects, color & looks** | Per-clip video (`blur`/`sharpen`/`hue`/`negate`/`vignette`/`chromakey`) and audio (`highpass`/`lowpass`/`EQ`/`compressor`/`gate`) chains, transform + color grade, and one-click **Punchy / Warm / Cool / Faded / B&W** looks. | | **Keyframe animation** | Animated zoom, position, rotation and opacity via piecewise-linear keyframes — the Transform panel auto-keyframes at the playhead. | | **360 reframing** | Cut a normal, flat shot out of **equirect or Insta360 dual-fisheye** footage: aim a virtual camera (yaw / pitch / roll / FOV) and **keyframe the pan**. An **Insta360 lens pair imports as one 360 asset** — Kerf stitches the two capture files into an equirect sphere once and caches it. Spherical sources are detected on import; anything unflagged can be marked by hand. | -| **Titles & captions** | Text overlays / lower-thirds with their own keyframes, and one-click **captions from a transcript** (SRT export too). | +| **Titles & captions** | Text overlays / lower-thirds with their own keyframes, Title / Lower-third / Caption style presets, the system font list, one-click **captions from a transcript**, and SRT export. | +| **Edit by transcript** | The transcript is an editing surface: lines resolve to the clip carrying them, a click seeks, and `×` cuts that sentence out of the timeline and ripples the gap closed. | | **Smart mixing** | Per-track **ducking** (music dips under dialogue via sidechain) and single-pass **loudnorm** to −14 LUFS on export. | -| **Export** | Positional, multi-track `filter_complex` with progress + cancel; **range export** renders just the region between your in/out marks. | -| **Agent workflow** | 60 MCP tools, a persisted task queue, and a fully revertible edit history attributed to user / agent / system. | +| **GPU-accelerated export** | NVENC / QSV / VideoToolbox / AMF are **verified with a real test encode** before being offered, hardware decode falls back to software on its own, and **range export** renders just the region between your in/out marks. | +| **Agent workflow** | 78 MCP tools, **staged edits with a reviewable diff**, a persisted task queue, and a fully revertible edit history attributed to user / agent / system. | +| **Auto-update** | The app updates itself from its own GitHub releases; a bundle only installs if its **minisign signature** verifies against the embedded public key. |
@@ -88,10 +94,13 @@ makes shows up **live** in your timeline for you to review. ### Download a build -Grab the installer for your platform from the -[**latest release**](https://github.com/OrellBuehler/kerf/releases/latest) — `.dmg`/`.app` -(macOS), `.AppImage`/`.deb` (Linux), or `.msi`/`.exe` (Windows). macOS and Linux builds -expect a system FFmpeg on `PATH`; the Windows build bundles it. +The [**Kerf site**](https://orellbuehler.github.io/kerf/#get) has a direct download +button for your platform, or grab the installer from the +[**latest release**](https://github.com/OrellBuehler/kerf/releases/latest) — `.dmg` +(macOS, Apple silicon + Intel), `.AppImage`/`.deb`/`.rpm` (Linux), or `.exe`/`.msi` +(Windows). macOS and Linux builds expect a system FFmpeg on `PATH`; the Windows build +bundles it. The macOS build is unsigned, so open it once with right-click → **Open**. +Once installed, Kerf updates itself from its own releases. ### Or build from source @@ -131,7 +140,7 @@ Now ask the agent to work on the project you have open — _"skim the interview the dead air, and drop in captions."_ It will claim a task, use the tools below, and hand back a reviewable result. -### The tools (59) +### The tools (78)
See / analyze @@ -146,8 +155,10 @@ the last three return images the model can see. Cut & arrange `cut_clip` · `add_clip_to_timeline` · `split_at` · `trim` · `reorder` · `move_clip` · -`remove` · `ripple_delete` · `cut_clip_range` · `add_track` · `remove_track` · -`set_track_duck` · `remove_silence` · `extract_audio` · `concatenate` +`remove` · `ripple_delete` · `cut_clip_range` · `duplicate_clips` · `set_clip_enabled` · +`add_track` · `remove_track` · `set_track_duck` · `set_track_muted` · `set_track_solo` · +`set_track_locked` · `add_marker` · `update_marker` · `remove_marker` · `remove_silence` · +`snap_to_beats` · `extract_audio` · `concatenate`
@@ -158,18 +169,20 @@ the last three return images the model can see. `add_keyframe` · `clear_keyframes` · `set_reframe` · `clear_reframe` · `set_reframe_keyframes` · `add_reframe_keyframe` · `add_overlay` · `update_overlay` · `remove_overlay` · `set_overlay_keyframes` · `captions_from_transcript` · `export_srt` · -`list_fonts` +`list_fonts` · `set_delivery_format` · `set_asset_projection`
Render & hand-off -`export` · `list_tasks` · `add_task` · `claim_next_task` · `complete_task` · `fail_task` · -`resolve_task` · `remove_task` · `history` · `undo` · `redo` · `revert_to` +`export` · `export_capabilities` · `stage_edits` · `staged_diff` · `apply_staged_edits` · +`discard_staged_edits` · `revision_diff` · `list_tasks` · `add_task` · `claim_next_task` · +`complete_task` · `fail_task` · `resolve_task` · `remove_task` · `history` · `undo` · +`redo` · `revert_to` · `transcription_status` · `download_speech_model`
-Every mutating tool emits a `project-changed` event, so edits appear in the GUI as the -agent makes them. +Every mutating tool emits a `project-changed` event, so the agent's proposal appears in +the GUI for review as it is built — the live cut only moves when *you* apply it. --- @@ -283,8 +296,7 @@ preview, transcript, analysis, effects, keyframes, captions, playback and export wired to real backend state, and the MCP surface is exercised end-to-end. **Roadmap:** a live activity stream pushed from the MCP server (the queue is polled -today), richer staged-edit diffs in the review step, and auralized effect chains in -preview playback. +today), transitions beyond crossfade / dip-to-black, and audible reverse shuttle. Contributions welcome — see [`CONTRIBUTING.md`](./CONTRIBUTING.md). diff --git a/docs/img/README.md b/docs/img/README.md index 48ddc18..f00eff5 100644 --- a/docs/img/README.md +++ b/docs/img/README.md @@ -17,7 +17,7 @@ timeline — a few clips across two or three tracks, a waveform, a scene marker | Filename | What to capture | Used by | | ---------------------------- | ------------------------------------------------------------------------------- | ------------------------ | -| `screenshot-editor.png` | The **full editor** window — title bar, media bin, preview, multi-track timeline. Hero shot. | README + site `og:image` | +| `screenshot-editor.png` | The **full editor** window — title bar, media bin, preview, multi-track timeline. Hero shot. | README | | `screenshot-agent.png` | The **agent panel** — task queue (a `ready` task with Apply/Dismiss) + history. | README | | `screenshot-inspector.png` | The **inspector** — an effects chain, the Transform/keyframe panel, or overlays. | README | @@ -31,6 +31,20 @@ Tips: - Keep the amber playhead visible — it's the brand's signature. Until these exist, the `README` shows broken-image placeholders where they go. The -landing site no longer embeds them — it renders animated CSS/SVG mockups instead — but -its `og:image` still points at `screenshot-editor.png`, so social-link previews stay -broken until the hero shot lands. +landing site no longer embeds them — it renders animated CSS/SVG mockups instead. + +## `og.png` — the social-card image + +`og.png` (1200×630) is the site's `og:image`, and it is **generated, not captured**: +[`../og-source.html`](../og-source.html) is the layout, rendered headless at exactly +that size. It lives one level up so the Pages build, which mounts *this* folder at +`static/img/`, does not publish it as a page. + +```bash +google-chrome --headless=new --disable-gpu --hide-scrollbars \ + --screenshot=docs/img/og.png --window-size=1200,630 \ + --virtual-time-budget=6000 "file://$PWD/docs/og-source.html" +``` + +The budget matters: the page pulls Space Grotesk / Inter / JetBrains Mono from Google +Fonts, and a shorter one screenshots the fallback faces. diff --git a/docs/img/og.png b/docs/img/og.png new file mode 100644 index 0000000..31637fa Binary files /dev/null and b/docs/img/og.png differ diff --git a/docs/og-source.html b/docs/og-source.html new file mode 100644 index 0000000..9a72982 --- /dev/null +++ b/docs/og-source.html @@ -0,0 +1,48 @@ + + + +
+
+
+ + + + + + Kerf +
+

The video editor
your AI can drive.

+

A non-destructive desktop NLE with an embedded MCP server — the agent + drives the same engine as the GUI, and can see the frames it edits.

+
NON-DESTRUCTIVE · MCP-NATIVE · FFMPEG-BACKED · macOS · WINDOWS · LINUX
+
+
+ + + +
+ diff --git a/site/assets/css/kerf.css b/site/assets/css/kerf.css index 4d74a4f..72188ba 100644 --- a/site/assets/css/kerf.css +++ b/site/assets/css/kerf.css @@ -187,7 +187,9 @@ pre { background: var(--surface-hover); } @media (max-width: 640px) { - .nav-links a:not(.ghost-link) { + /* Only the download chip survives — it is the primary action, and the + section links are all reachable by scrolling anyway. */ + .nav-links a:not(.nav-dl) { display: none; } } @@ -221,8 +223,8 @@ pre { h1 { font-family: var(--font-display); font-weight: 700; - font-size: clamp(38px, 6.4vw, 68px); - line-height: 1.02; + font-size: clamp(36px, 5.4vw, 60px); + line-height: 1.04; letter-spacing: -0.025em; margin: 18px 0 0; } @@ -689,6 +691,12 @@ code { border-bottom-color: rgba(226, 157, 46, 0.4); } +/* The open download menu has to clear the band below it: every `.wrap` sits at + z-index 1, so DOM order alone would paint the stat strip over the hero. */ +.hero .wrap { + z-index: 20; +} + /* Wider hero on large screens: copy + shot side by side */ @media (min-width: 1000px) { .hero { @@ -1461,6 +1469,41 @@ html.js .stagger.in > *:nth-child(n + 9) { transition-delay: 540ms; } 89% { box-shadow: 0 0 0 1px rgba(234, 177, 78, 0.55), 0 0 34px rgba(226, 157, 46, 0.25); } 94%, 100% { box-shadow: var(--shadow-lg); } } +.ms-diff { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px 10px; + padding: 8px 14px; + border-top: 1px solid var(--border-subtle); + font-size: 10px; +} +.ms-diff .lead { + color: var(--text-secondary); + font-weight: 500; +} +.ms-diff i { + font-style: normal; + padding: 1px 6px; + border-radius: 3px; + border: 1px solid transparent; +} +.ms-diff .add { + color: #6fd39b; + background: rgba(40, 197, 133, 0.1); + border-color: rgba(40, 197, 133, 0.28); +} +.ms-diff .cut { + color: #f08a78; + background: rgba(241, 84, 63, 0.1); + border-color: rgba(241, 84, 63, 0.28); +} +.ms-diff .shift { + color: var(--kerf-300); + background: rgba(226, 157, 46, 0.1); + border-color: rgba(226, 157, 46, 0.3); +} + .ms-foot { padding: 9px 14px; border-top: 1px solid var(--border-subtle); @@ -1610,6 +1653,402 @@ html.js .stagger.in > *:nth-child(n + 9) { transition-delay: 540ms; } border-color: var(--border-strong); } +/* ============================================================= + Downloads. + The links ship pointing at the releases page and are rewritten + in place by assets/js/download.js once it has resolved the real + asset URLs — so every state below has to read correctly with + the script blocked, too. + ============================================================= */ + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* ---- The split button ---- */ +.dl { + position: relative; + display: inline-flex; + align-items: stretch; +} +.dl-main { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + padding-right: 18px; +} +.dl-ic { + width: 15px; + height: 15px; + flex: none; + opacity: 0.9; +} +.dl-toggle { + position: relative; + padding: 0 12px; + margin-left: 1px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + box-shadow: 0 8px 26px rgba(226, 157, 46, 0.28); + cursor: pointer; + font: inherit; +} +/* One seam between the two halves, drawn on the dark button face. */ +.dl-toggle::before { + content: ""; + position: absolute; + left: -1px; + top: 9px; + bottom: 9px; + width: 1px; + background: rgba(26, 18, 5, 0.25); +} +.dl-chev { + width: 14px; + height: 14px; + transition: transform 160ms var(--ease-out); +} +.dl-toggle[aria-expanded="true"] .dl-chev { + transform: rotate(180deg); +} + +/* ---- The platform menu ---- */ +.dl-menu { + position: absolute; + z-index: 30; + top: calc(100% + 10px); + left: 0; + min-width: 290px; + padding: 8px; + background: var(--surface-raised); + border: 1px solid var(--border-strong); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-pop); +} +.dl-menu[hidden] { + display: none; +} +html.js .dl-menu:not([hidden]) { + animation: dl-pop 150ms var(--ease-out); +} +@keyframes dl-pop { + from { + opacity: 0; + transform: translateY(-6px); + } +} +.dl-menu-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 10px; + padding: 4px 10px 8px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-muted); +} +.dl-ver { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0; + text-transform: none; + color: var(--kerf-400); +} +.dl-group + .dl-group { + margin-top: 6px; + padding-top: 6px; + border-top: 1px solid var(--border-subtle); +} +.dl-group-head { + display: flex; + align-items: center; + gap: 8px; + padding: 5px 10px 4px; + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); +} +.os-ic { + flex: none; + opacity: 0.75; +} +.dl-menu-foot { + display: block; + margin-top: 6px; + padding: 9px 10px 5px; + border-top: 1px solid var(--border-subtle); + font-size: 12.5px; + color: var(--text-muted); +} +.dl-menu-foot:hover { + color: var(--kerf-400); +} + +/* ---- One downloadable file ---- */ +.dl-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + padding: 8px 10px; + border-radius: var(--radius-lg); + font-size: 14px; + color: var(--text-primary); + transition: + background 120ms, + color 120ms; +} +.dl-row[hidden] { + display: none; +} +.dl-row:hover { + background: var(--surface-hover); +} +.dl-row-name { + font-weight: 500; +} +.dl-row-meta { + display: inline-flex; + align-items: baseline; + gap: 9px; + font-family: var(--font-mono); + font-size: 11.5px; + color: var(--text-muted); + white-space: nowrap; +} +.dl-ext { + color: var(--text-secondary); +} +.dl-size:empty { + display: none; +} +/* The row the hero button points at. */ +.dl-row.is-pick .dl-row-name::after { + content: "Detected"; + margin-left: 9px; + padding: 1px 7px; + border-radius: var(--radius-full); + background: rgba(226, 157, 46, 0.13); + border: 1px solid rgba(226, 157, 46, 0.3); + color: var(--kerf-300); + font-family: var(--font-sans); + font-size: 10.5px; + font-weight: 600; + letter-spacing: 0.03em; + vertical-align: 1px; +} + +/* ---- Hero meta lines ---- */ +.dl-meta { + margin: 14px 0 0; + font-family: var(--font-mono); + font-size: 12.5px; + color: var(--kerf-300); +} +.dl-meta[hidden] { + display: none; +} +.dl-note { + margin: 10px 0 0; + font-size: 13px; + color: var(--text-muted); +} +.dl-note a { + color: var(--text-secondary); + border-bottom: 1px solid var(--border-strong); +} +.dl-note a:hover { + color: var(--kerf-400); +} +.get-cta { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: 16px; + margin-top: 26px; +} +.get-cta .dl-meta { + margin: 0; +} + +/* ---- The Get-it platform cards ---- */ +.dl-cards { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 18px; + margin-bottom: 22px; +} +@media (max-width: 860px) { + .dl-cards { + grid-template-columns: 1fr; + } +} +.dl-card { + display: flex; + flex-direction: column; + min-width: 0; + padding: 20px 18px 18px; + background: var(--surface-raised); + border: 1px solid var(--border-default); + border-radius: var(--radius-2xl); + transition: + border-color 200ms, + transform 200ms var(--ease-out); +} +.dl-card:hover { + transform: translateY(-2px); + border-color: var(--border-strong); +} +.dl-card.is-mine { + border-color: rgba(226, 157, 46, 0.4); + box-shadow: 0 0 0 1px rgba(226, 157, 46, 0.12); +} +.dl-card header { + display: flex; + align-items: center; + gap: 9px; + margin-bottom: 12px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border-subtle); +} +.dl-card h3 { + flex: 1; + margin: 0; + font-family: var(--font-display); + font-weight: 600; + font-size: 17px; +} +.dl-card .os-ic { + opacity: 0.85; +} +.dl-mine { + display: none; + padding: 2px 9px; + border-radius: var(--radius-full); + background: rgba(226, 157, 46, 0.13); + border: 1px solid rgba(226, 157, 46, 0.3); + color: var(--kerf-300); + font-size: 10.5px; + font-weight: 600; + letter-spacing: 0.03em; +} +.dl-card.is-mine .dl-mine { + display: inline-block; +} +.dl-card .dl-row { + margin: 0 -8px; + padding: 8px; +} +.dl-card .dl-row.is-pick .dl-row-name::after { + content: none; +} +.dl-card .fine { + /* auto so the fine print sits on one baseline across cards of unequal + row counts; the padding keeps a gap when there is no slack to take. */ + margin: auto 0 0; + padding-top: 16px; + border-top: 1px solid var(--border-subtle); + font-size: 12.5px; + line-height: 1.55; + color: var(--text-muted); +} +.dl-card .fine code { + font-size: 0.92em; +} + +/* ============================================================= + Stat strip — the numbers that are actually true. + ============================================================= */ +.stats-band { + border-top: 1px solid var(--border-subtle); + border-bottom: 1px solid var(--border-subtle); + background: rgba(10, 12, 15, 0.4); +} +.stats { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + padding-top: 26px; + padding-bottom: 26px; +} +@media (max-width: 780px) { + .stats { + grid-template-columns: repeat(2, 1fr); + gap: 20px 8px; + } +} +.stat { + display: flex; + flex-direction: column; + gap: 4px; + padding-left: 16px; + border-left: 1px solid var(--border-subtle); +} +.stat b { + font-family: var(--font-display); + font-size: clamp(19px, 2.4vw, 26px); + font-weight: 700; + letter-spacing: -0.02em; + color: var(--kerf-400); + line-height: 1.1; +} +.stat span { + font-size: 12.5px; + line-height: 1.45; + color: var(--text-muted); +} +.stat:first-child { + padding-left: 0; + border-left: 0; +} +@media (max-width: 780px) { + .stat:nth-child(3) { + padding-left: 0; + border-left: 0; + } +} + +/* Nav: a download link that reads as the primary action. */ +.nav-dl { + padding: 7px 15px; + border-radius: var(--radius-full); + background: var(--kerf-500); + color: #1a1205 !important; + font-weight: 600; +} +.nav-dl:hover { + background: var(--kerf-400); +} +@media (max-width: 640px) { + .nav-links { + gap: 12px; + } +} + +/* The staged-review tool reads as neither "sees" nor plain. */ +.chip.stage { + color: var(--kerf-300); + border-color: rgba(226, 157, 46, 0.28); + background: rgba(226, 157, 46, 0.07); +} + +/* Keyboard focus was invisible on the dark surfaces. */ +:focus-visible { + outline: 2px solid var(--kerf-400); + outline-offset: 2px; + border-radius: var(--radius-md); +} + /* ---- Reduced motion: freeze everything on its legible still ---- */ @media (prefers-reduced-motion: reduce) { html.js .reveal, @@ -1634,4 +2073,7 @@ html.js .stagger.in > *:nth-child(n + 9) { transition-delay: 540ms; } .flow-svg .pulse { display: none; } + html.js .dl-menu:not([hidden]) { + animation: none; + } } diff --git a/site/assets/js/download.js b/site/assets/js/download.js new file mode 100644 index 0000000..a5d33cb --- /dev/null +++ b/site/assets/js/download.js @@ -0,0 +1,281 @@ +/* ============================================================= + Direct downloads. + + Release assets carry their version in the file name + (`Kerf_0.19.1_x64-setup.exe`), so GitHub's stable + `releases/latest/download/` URL cannot address them. This + resolves the real asset URLs from the API instead, points the + hero button at the visitor's own platform, and fills in the + version and size on every platform row. + + Progressive enhancement: every link ships pointing at the + releases page, so the page stays usable with this file blocked + or the API unreachable. + ============================================================= */ +(function () { + "use strict"; + + var root = document.querySelector("[data-dl]"); + if (!root || !window.fetch) return; + + var REPO = root.getAttribute("data-repo"); + if (!REPO) return; + + function all(sel) { + return Array.prototype.slice.call(root.querySelectorAll(sel)); + } + + /* --- The asset table ------------------------------------------------- + One entry per downloadable file we publish. `match` recognizes the + asset by name, so `.sig` files and latest.json never match. */ + var ASSETS = [ + { key: "windows-exe", os: "windows", sub: "Installer (.exe)", match: /_x64-setup\.exe$/i }, + { key: "windows-msi", os: "windows", sub: "MSI package", match: /_x64_en-US\.msi$/i }, + { key: "macos-arm", os: "macos", sub: "Apple silicon (.dmg)", match: /_aarch64\.dmg$/i }, + { key: "macos-intel", os: "macos", sub: "Intel (.dmg)", match: /_x64\.dmg$/i }, + { key: "linux-appimage", os: "linux", sub: "AppImage", match: /_amd64\.AppImage$/i }, + { key: "linux-deb", os: "linux", sub: "Debian / Ubuntu (.deb)", match: /_amd64\.deb$/i }, + { key: "linux-rpm", os: "linux", sub: "Fedora / RHEL (.rpm)", match: /\.x86_64\.rpm$/i } + ]; + + /* Which file a visitor on this platform actually wants. */ + var PREFERRED = { + windows: "windows-exe", + "macos:arm64": "macos-arm", + "macos:x64": "macos-intel", + linux: "linux-appimage" + }; + + var OS_NAME = { windows: "Windows", macos: "macOS", linux: "Linux" }; + + /* --- Platform detection ---------------------------------------------- */ + + function detectOs() { + var ua = navigator.userAgent || ""; + // Phones and tablets get no desktop build: leave them undetected so the + // button stays a neutral "Download" pointing at the release page. + if (/Android|iPhone|iPad|iPod/i.test(ua)) return ""; + var hint = (navigator.userAgentData && navigator.userAgentData.platform) || ""; + var p = hint || navigator.platform || ""; + if (/Win/i.test(p) || /Windows/i.test(ua)) return "windows"; + if (/Mac/i.test(p) || /Mac OS X/i.test(ua)) return "macos"; + if (/Linux|X11|CrOS/i.test(p) || /Linux|X11|CrOS/i.test(ua)) return "linux"; + return ""; + } + + // Safari exposes no architecture hint and every Mac still reports + // "MacIntel" in navigator.platform — so ask the GPU. Apple silicon renders + // through "Apple GPU" / "Apple M…"; an Intel Mac names Intel, AMD or NVIDIA. + function macIsAppleSilicon() { + try { + var gl = + document.createElement("canvas").getContext("webgl") || + document.createElement("canvas").getContext("experimental-webgl"); + if (!gl) return null; + var dbg = gl.getExtension("WEBGL_debug_renderer_info"); + if (!dbg) return null; + var r = String(gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) || ""); + if (/Intel|AMD|Radeon|NVIDIA|GeForce/i.test(r)) return false; + if (/Apple/i.test(r)) return true; + } catch (e) { + /* blocked by a privacy setting — fall through to "unknown" */ + } + return null; + } + + /* --- Release lookup --------------------------------------------------- + Not `/releases/latest`: publishing a tag makes it "latest" some 25 + minutes before the slowest bundle finishes uploading, so the newest + release can legitimately carry no assets yet. Walk back to the newest + one that actually has files. */ + + var CACHE_KEY = "kerf.dl.v1"; + var CACHE_MS = 30 * 60 * 1000; + + function cached() { + try { + var v = JSON.parse(sessionStorage.getItem(CACHE_KEY) || "null"); + return v && Date.now() - v.at < CACHE_MS ? v.data : null; + } catch (e) { + return null; + } + } + + function store(data) { + try { + sessionStorage.setItem(CACHE_KEY, JSON.stringify({ at: Date.now(), data: data })); + } catch (e) { + /* private mode — just skip the cache */ + } + } + + function resolve(rel) { + var files = {}; + var oses = {}; + (rel.assets || []).forEach(function (a) { + ASSETS.forEach(function (spec) { + if (!files[spec.key] && spec.match.test(a.name)) { + files[spec.key] = { url: a.browser_download_url, size: a.size }; + oses[spec.os] = true; + } + }); + }); + var n = Object.keys(files).length; + if (!n) return null; + return { + tag: rel.tag_name, + url: rel.html_url, + files: files, + complete: oses.windows && oses.macos && oses.linux + }; + } + + /* The newest release carrying all three platforms. A release whose slowest + bundle is still uploading has some but not all, and offering it would + leave whoever is on the missing platform with no direct link at all — so + fall back to the newest complete one, and only settle for a partial + release if the whole fetched window is partial. */ + function pickRelease(list) { + var partial = null; + for (var i = 0; i < list.length; i++) { + if (list[i].draft || list[i].prerelease) continue; + var got = resolve(list[i]); + if (!got) continue; + if (got.complete) return got; + if (!partial) partial = got; + } + return partial; + } + + function fetchRelease() { + var hit = cached(); + if (hit) return Promise.resolve(hit); + return fetch("https://api.github.com/repos/" + REPO + "/releases?per_page=10", { + headers: { Accept: "application/vnd.github+json" } + }) + .then(function (r) { + if (!r.ok) throw new Error("github " + r.status); + return r.json(); + }) + .then(function (list) { + var data = Array.isArray(list) && pickRelease(list); + if (!data) throw new Error("no release with assets"); + store(data); + return data; + }); + } + + /* --- Rendering -------------------------------------------------------- */ + + function mb(bytes) { + if (!bytes) return ""; + var m = bytes / 1048576; + return (m >= 100 ? Math.round(m) : m.toFixed(1)) + " MB"; + } + + function specFor(key) { + for (var i = 0; i < ASSETS.length; i++) if (ASSETS[i].key === key) return ASSETS[i]; + return null; + } + + function apply(data) { + root.classList.add("dl-live"); + + // Every platform row on the page — hero menu and the Get-it grid alike. + ASSETS.forEach(function (spec) { + var file = data.files[spec.key]; + all('[data-dl-asset="' + spec.key + '"]').forEach(function (row) { + if (!file) { + row.setAttribute("hidden", ""); + return; + } + row.removeAttribute("hidden"); + row.href = file.url; + row.removeAttribute("target"); + row.removeAttribute("rel"); + var size = row.querySelector("[data-dl-size]"); + if (size) size.textContent = mb(file.size); + }); + }); + + all("[data-dl-version]").forEach(function (el) { + el.textContent = data.tag; + el.removeAttribute("hidden"); + }); + + var os = detectOs(); + if (!os) return; + + var wantKey = PREFERRED[os]; + if (os === "macos") { + // Unknown architecture keeps Apple silicon — the overwhelming majority + // of Macs in use — and the Intel build stays one click away in the menu. + wantKey = PREFERRED[macIsAppleSilicon() === false ? "macos:x64" : "macos:arm64"]; + } + + var spec = specFor(wantKey); + var file = spec && data.files[spec.key]; + if (!file) return; + + all("[data-dl-primary]").forEach(function (main) { + main.href = file.url; + main.removeAttribute("target"); + main.removeAttribute("rel"); + var label = main.querySelector("[data-dl-label]"); + if (label) label.textContent = "Download for " + OS_NAME[spec.os]; + }); + + all("[data-dl-meta]").forEach(function (meta) { + meta.textContent = [data.tag, spec.sub, mb(file.size)].filter(Boolean).join(" · "); + meta.removeAttribute("hidden"); + }); + + // Show the visitor where the button points. + all('[data-dl-asset^="' + spec.os + '"]').forEach(function (row) { + row.classList.add("is-mine"); + }); + all('[data-dl-asset="' + spec.key + '"]').forEach(function (row) { + row.classList.add("is-pick"); + }); + all('[data-dl-card="' + spec.os + '"]').forEach(function (card) { + card.classList.add("is-mine"); + }); + } + + /* --- The split button's menu ------------------------------------------ */ + + function wireMenu(menu) { + var wrap = menu.closest(".dl"); + var toggle = wrap && wrap.querySelector("[data-dl-toggle]"); + if (!toggle) return; + + function open(on) { + toggle.setAttribute("aria-expanded", on ? "true" : "false"); + if (on) menu.removeAttribute("hidden"); + else menu.setAttribute("hidden", ""); + } + + toggle.addEventListener("click", function (e) { + e.preventDefault(); + open(toggle.getAttribute("aria-expanded") !== "true"); + }); + document.addEventListener("click", function (e) { + if (!wrap.contains(e.target)) open(false); + }); + document.addEventListener("keydown", function (e) { + if (e.key === "Escape") open(false); + }); + menu.addEventListener("click", function (e) { + if (e.target && e.target.closest && e.target.closest("a")) open(false); + }); + open(false); + } + + all("[data-dl-menu]").forEach(wireMenu); + + fetchRelease().then(apply, function () { + /* Offline, rate-limited, or no published assets: the links already point + at the releases page, which is the right destination anyway. */ + root.classList.add("dl-fallback"); + }); +})(); diff --git a/site/hugo.toml b/site/hugo.toml index a1d4e86..b433250 100644 --- a/site/hugo.toml +++ b/site/hugo.toml @@ -7,11 +7,16 @@ enableRobotsTXT = true disableKinds = ["taxonomy", "term", "RSS", "sitemap"] [params] - description = "Kerf is a non-destructive, FFmpeg-backed desktop video editor with an embedded MCP server, so an LLM can analyze your footage and assemble the cut through the same engine the GUI uses — and can see the frames it edits." + description = "Kerf is a non-destructive, FFmpeg-backed desktop video editor with an embedded MCP server, so an LLM can analyze your footage and assemble the cut through the same engine the GUI uses — and can see the frames it edits. Free download for macOS, Windows and Linux." tagline = "The non-destructive video editor your AI can drive." github = "https://github.com/OrellBuehler/kerf" + # owner/name — download.js resolves release assets through the GitHub API, + # because the asset file names carry the version and so cannot be addressed + # by the stable releases/latest/download/ URL. + repo = "OrellBuehler/kerf" releases = "https://github.com/OrellBuehler/kerf/releases/latest" mcpUrl = "http://127.0.0.1:7777/mcp" + toolCount = 78 [minify] minifyOutput = true diff --git a/site/layouts/_default/baseof.html b/site/layouts/_default/baseof.html index 4c4a6e4..8f41df8 100644 --- a/site/layouts/_default/baseof.html +++ b/site/layouts/_default/baseof.html @@ -6,10 +6,18 @@ {{ .Title }} + + + - + {{/* docs/img is mounted at static/img, so this is the same file the repo + README can use — see docs/img/README.md for how it is regenerated. */}} + + + + @@ -46,5 +54,7 @@ io.observe(el); }); + {{ $dl := resources.Get "js/download.js" | minify | fingerprint }} + diff --git a/site/layouts/index.html b/site/layouts/index.html index 2cb9796..8912065 100644 --- a/site/layouts/index.html +++ b/site/layouts/index.html @@ -2,6 +2,7 @@ {{ $gh := .Site.Params.github }} {{ $rel := .Site.Params.releases }} {{ $mcp := .Site.Params.mcpUrl }} +{{ $tools := .Site.Params.toolCount }} @@ -14,13 +15,16 @@
-
+{{/* One `data-dl` scope for the whole page: download.js fills the hero menu and + the Get-it cards from a single GitHub API call. */}} +
@@ -34,10 +38,12 @@

The video editor
your AI can drive.

it edits. Nothing is re-encoded until you export.

- Download + {{ partial "download-button.html" (dict "Site" .Site "id" "hero") }} View source - How the agent edits →
+ +

Free for noncommercial use · macOS · Windows · Linux · + all builds & requirements

{{ partial "mock-editor.html" . }} @@ -46,6 +52,16 @@

The video editor
your AI can drive.

+ +
+
+
{{ $tools }}MCP tools over one live project
+
0re-encodes before you export
+
3platforms, signed auto-updates
+
16:9 · 9:16 · 1:1delivery frames with safe areas
+
+
+
@@ -55,8 +71,8 @@

Most tools bolt a chatbot onto an editor.
Kerf makes the editor an 

The same Project is exposed twice — as the GUI you click and as Model Context Protocol - tools an agent calls. It doesn't script a black box; it drives the identical engine, - and every edit shows up live for you to review. + tools an agent calls. It doesn't script a black box; it drives the identical engine — + and its edits are staged for review, never applied behind your back.

@@ -75,21 +91,21 @@

The agent has eyes

🔗

Same engine, same project

-

The MCP server and the webview hold the same project behind one lock. When - the agent trims a clip, your GUI re-renders it instantly — no import/export - round-trip, no second copy of the truth.

+

The MCP server and the webview hold the same project behind one lock. No + import/export round-trip, no second copy of the truth — and the agent previews its + own work through the exact graph that renders your export.

-
🤝
-

You stay in control

-

Work is handed back through a task queue: you enqueue plain-language tasks, - the agent marks one ready, and you apply or dismiss. Kerf never edits on its - own, and every change is revertible.

+
📋
+

Staged, then reviewed

+

Agent edits build a proposal, not your cut. The review card shows a real + diff — added, cut, moved, retrimmed — and you can watch the proposed timeline + play, then apply it as one revertible revision, or discard it.

{{ partial "mock-session.html" . }} -
One loop of a real session: skim → cut → preview → hand back. You apply — or dismiss.
+
One loop of a real session: skim → cut → preview → hand back a diff. You apply — or discard.
@@ -102,7 +118,8 @@

You stay in control

A real NLE — not a scaffold.

Everything below is wired to real backend state and renders from a pure, unit-tested export graph. Clip positions, gaps, track layering, effects, keyframes, - overlays, ducking and loudnorm all come out of one filter_complex.

+ overlays, ducking and loudnorm all come out of one filter_complex — + the same one that draws your preview.

{{ partial "edl-diagram.html" . }}
@@ -110,15 +127,18 @@

A real NLE — not a scaffold.

keyframes edit that data — sources are read once, at export.
- {{ partial "feature.html" (dict "t" "Multi-track NLE" "d" "Bespoke timeline: video / audio / text tracks, free positioning with gaps, drag across tracks, edge-drag trim, razor split, ripple delete, snapping to edges, the playhead and beats.") }} - {{ partial "feature.html" (dict "t" "Audible playback" "d" "Real Web-Audio playback with J/K/L shuttle and scrub; volume, fades, speed and reverse are auralized and the playhead follows the audio clock.") }} - {{ partial "feature.html" (dict "t" "Analysis" "d" "FFmpeg silence detection, scene detection, an audio energy / beat grid and waveforms — all CLI-driven. Optional local Whisper transcription.") }} - {{ partial "feature.html" (dict "t" "Effects & color" "d" "Per-clip video (blur / sharpen / hue / negate / vignette / chroma-key) and audio (highpass / lowpass / EQ / compressor / gate) chains, plus transform and color grade.") }} - {{ partial "feature.html" (dict "t" "Keyframe animation" "d" "Animated zoom, position, rotation and opacity via piecewise-linear keyframes — the Transform panel auto-keyframes at the playhead.") }} - {{ partial "feature.html" (dict "t" "360 reframing" "d" "Cut a normal, flat shot out of equirect or Insta360 dual-fisheye footage: aim a virtual camera with yaw / pitch / roll / FOV and keyframe the pan. Spherical sources are detected on import.") }} - {{ partial "feature.html" (dict "t" "Titles & captions" "d" "Text overlays and lower-thirds with their own keyframes, plus one-click captions from a transcript and SRT export.") }} - {{ partial "feature.html" (dict "t" "Smart mixing" "d" "Per-track ducking dips music under dialogue via sidechain, and a single-pass loudnorm brings the final mix to −14 LUFS on export.") }} - {{ partial "feature.html" (dict "t" "Export" "d" "Positional, multi-track filter_complex with progress and cancel; range export renders just the region between your in / out marks.") }} + {{ partial "feature.html" (dict "t" "Multi-track NLE" "d" "Video / audio / text tracks with free positioning and gaps, drag across tracks, edge-drag trim, razor split, ripple delete, markers, mute / solo / lock, and snapping to edges, the playhead and beats.") }} + {{ partial "feature.html" (dict "t" "Real playback" "d" "Not a slideshow: one long-lived FFmpeg streams the composited cut — every track, effect, keyframe and overlay — paced against the audio clock, with J/K/L shuttle and audible Web-Audio scrub.") }} + {{ partial "feature.html" (dict "t" "Delivery frames" "d" "Cut for the shape you ship: 16:9, 9:16, 1:1 or 4:5 is a property of the project, so preview, scrub and export all render the same frame — with platform safe-area guides and cover-crop instead of letterboxing.") }} + {{ partial "feature.html" (dict "t" "Analysis & speech" "d" "Silence and scene detection, an onset / tempo grid, EBU R128 loudness and speech-vs-music from one decode — plus transcription in every build, via FFmpeg's whisper filter or in-process whisper.cpp.") }} + {{ partial "feature.html" (dict "t" "Cut to the beat" "d" "Tempo is mapped onto timeline time as a confidence-gated beat grid the ruler draws and drags snap to — then one call ripples a whole track's cuts onto it, retrimming each clip at its outgoing edge.") }} + {{ partial "feature.html" (dict "t" "Effects, color & looks" "d" "Per-clip video (blur / sharpen / hue / vignette / chroma-key) and audio (highpass / lowpass / EQ / compressor / gate) chains, a transform and color grade, and one-click Punchy / Warm / Cool / Faded / B&W looks.") }} + {{ partial "feature.html" (dict "t" "Keyframe animation" "d" "Zoom, position, rotation and opacity animate over piecewise-linear keyframes — the Transform panel auto-keyframes at the playhead and shows the sampled pose.") }} + {{ partial "feature.html" (dict "t" "360 & Insta360" "d" "Cut a normal, flat shot out of spherical footage: aim a virtual camera with yaw / pitch / roll / FOV and keyframe the pan. An Insta360 lens pair imports as one asset — Kerf stitches the two fisheyes into an equirect sphere and caches it.") }} + {{ partial "feature.html" (dict "t" "Titles & captions" "d" "Text overlays and lower-thirds with their own keyframes, Title / Lower-third / Caption style presets, the system font list, one-click captions from a transcript, and SRT export.") }} + {{ partial "feature.html" (dict "t" "Edit by transcript" "d" "The transcript is an editing surface: lines resolve to the clip carrying them, a click seeks, and × cuts that sentence out of the timeline and ripples the gap closed.") }} + {{ partial "feature.html" (dict "t" "Smart mixing" "d" "Per-track ducking dips music under dialogue via sidechain compression, and a single-pass loudnorm brings the final mix to −14 LUFS on export.") }} + {{ partial "feature.html" (dict "t" "GPU-accelerated export" "d" "NVENC / QSV / VideoToolbox / AMF are verified with a real test encode before they are offered, hardware decode falls back to software on its own, and range export renders just your in / out marks.") }}
@@ -127,25 +147,28 @@

A real NLE — not a scaffold.

- 59 tools, one live project + {{ $tools }} tools, one live project

Connect any MCP client
and start editing.

The desktop app hosts the server over streamable HTTP. Point a client at it and ask in plain language — “skim the interview, cut the dead air, drop in - captions.” It claims a task, works, and hands back a reviewable result.

+ captions.” It claims a task, works, and hands back a diff you review.

get_frame skim_asset preview_timeline + analyze_asset cut_clip - split_at ripple_delete remove_silence + snap_to_beats set_video_effects add_keyframe + set_reframe captions_from_transcript - set_track_duck + set_delivery_format + staged_diff export - +43 more + +{{ sub $tools 15 }} more
@@ -170,22 +193,34 @@

Connect any MCP client
and start editing.

Get it -

Download a build, or run from source.

+

Download Kerf.

+

Installers for every platform, straight from the latest GitHub release — + no account needed. Once installed, Kerf keeps itself up to date over a + signature-verified update feed.

+
+ {{ partial "download-button.html" (dict "Site" .Site "id" "get") }} + +
+ + {{ partial "download-grid.html" . }} +
-
-

Download

-

Installers for macOS, Linux and Windows are published on every release.

-

macOS / Linux expect a system FFmpeg on PATH; the Windows build bundles it.

- Latest release ↗ -

Build from source

git clone {{ $gh }}
-cd kerf/frontend && bun install && cd ..
+cd kerf/frontend && bun install && cd ..
 cargo run -p kerf-app

Needs Rust ≥ 1.95, Bun ≥ 1.2, and (for media) the FFmpeg dev libraries. - Builds without them via --no-default-features.

+ Builds without them via --no-default-features — the engine drives the + ffmpeg / ffprobe binaries either way.

+
+
+

Then point an agent at it

+
claude mcp add --transport http \
+  kerf {{ $mcp }}
+

The app hosts the MCP server itself — there is no second process to + start. Queue a task in the agent panel, and review the diff it hands back.

@@ -200,6 +235,7 @@

Build from source

non-destructive video editing, driven by AI
diff --git a/site/layouts/partials/os-icon.html b/site/layouts/partials/os-icon.html new file mode 100644 index 0000000..e888b26 --- /dev/null +++ b/site/layouts/partials/os-icon.html @@ -0,0 +1,11 @@ +{{- /* Platform glyphs for the download surfaces. `.os` is windows | macos | linux. */ -}} +{{- $h := default 16 .h -}} +