# OpenCut: the 92K-star repo is a rewrite, and the editor you can use is the archive

> Satyajit Ghana — Head of Engineering @ Inkers Technology
> canonical: https://ai.thesatyajit.com/articles/opencut
> date: 2026-10-06
> tags: video, webgpu, rust, browser-ml, creative-tools

A post on X went around this week: *"Someone open-sourced a free CapCut alternative with no watermarks. OpenCut packs multitrack editing, plugins, AI agents, automations, and cross-platform support into a free video editor with 92K GitHub stars."* It links [`OpenCut-app/OpenCut`](https://github.com/OpenCut-app/OpenCut). The thread has no follow-up posts. The most useful reply was not about features: *"reopen a six-month-old project and export it without hunting for missing assets."*

So I did three things. I cloned the linked repository at `e668010` (2026-09-24). I cloned the one its README points to, [`OpenCut-app/opencut-classic`](https://github.com/OpenCut-app/opencut-classic), at `cf5e79e` (2026-05-17). And I used the hosted editor at `opencut.app`: imported two generated clips, added a text layer and exported. Every number below is labelled **measured** (I computed it from a file or a run), **reported** (someone else's figure, not re-run) or **reasoned** (my arithmetic on the other two).

The short version: the post merges two codebases. The editor you can open today lives in an archived repository. The plugins, the agents and the one-codebase-everywhere story are the README's list of what the rewrite will bring. The 92K stars sit on the rewrite.

<RepoCard repo="OpenCut-app/OpenCut" />

## Two repositories, one name

The linked repository's README opens with a status section. Its first line: *"OpenCut is being rewritten from the ground up."* Then a list under "What's coming": an Editor API, first-class third-party plugins, *"Desktop, mobile, and browser from one codebase (Rust core)"*, an MCP server *"for AI agents"*, a headless mode *"automation, batch rendering"*, and a scripting tab. It then says the previous version, at `opencut-classic`, *"is the one to reach for today"*, and that `opencut.app` still runs it.

That list is where the post's "plugins, AI agents, automations, and cross-platform support" come from. Here is what the rewrite contains today:

| Path in `OpenCut-app/OpenCut` | What it is | Measured |
|---|---|---|
| `apps/web` | TanStack Start + Vite. `/` renders `hello world!`. `/editor` renders `Coming soon.` | 2 routes |
| `apps/web/src/components/ui` | Vendored shadcn/ui components | 6,763 lines |
| `apps/desktop` | A GPUI app. Its README: *"Very early. Right now this is just a window that opens."* Panels named Timeline, Preview, Browser, Inspector each render their own name | 12 Rust files |
| `apps/api` | An Elysia worker with `/`, `/health` and `/echo` | 15 lines |
| `crates/media` | Only `setup/`: a script that fetches and checksums FFmpeg 8.1.3 for four platforms | no Rust source |

The whole checkout is 130 files (measured). Of its 10,498 non-lockfile lines, 6,763 are UI primitives (measured), so about 64% (reasoned). There is no timeline, no renderer and no plugin host yet. The README also says the project is *"not set up to take outside contributions yet while the architecture is being designed."*

The star counts make the split sharper. GitHub's page showed 92.9K stars, 9.1K forks and 1,599 commits on the rewrite, and 265 stars, 339 forks and 1,567 commits on the classic repository, archived on 2026-05-17 (reported, read from github.com on 2026-10-06). The near-equal commit counts suggest the rewrite keeps the old history and replaced the tree, but a shallow clone cannot confirm that, so treat it as a guess. The stars followed the name, not the code. The landing page at `opencut.app` still says "70k+ stars" on its badge (measured, screenshot below).

<Figure
  src="https://ai.thesatyajit.com/articles/opencut/fig3.png"
  alt="The opencut.app landing page. The headline reads 'The open source Video editor', with 'A simple but powerful video editor that gets the job done. Works on any platform.' beneath it and a 'Try early beta' button. A badge at the top right reads '70k+ stars' and a dark 'Projects' button sits next to it."
  caption="opencut.app on 2026-10-06. It runs the classic editor, not the rewrite, and its star badge lags the repository (opencut.app landing page, my screenshot)."
/>

The rewrite's one concrete technical decision is in `crates/media/setup/ffmpeg.json`. It pins FFmpeg 8.1.3 by source hash, plus prebuilt `lgpl-shared` builds for Windows and Linux on x86_64 and aarch64 (measured). Shared LGPL builds are the configuration that lets an MIT-licensed app link FFmpeg dynamically without taking on the GPL. A native core will decode with FFmpeg instead of the browser's codecs. That matters for the "one codebase" promise: a desktop app built on FFmpeg and a web app built on WebCodecs need two decode paths under one API. None of that exists yet.

## The editor that exists

The classic repository is the editor. It is a Next.js 16 app with a Rust core compiled to WebAssembly: 685 TypeScript files holding 91,028 lines, plus 4,337 lines of Rust and 438 lines of WGSL shaders (measured). Everything below is read from that tree. I also ran it through the hosted copy at `opencut.app`, which says it runs the classic build. I did not build or run the code from the clone.

<Figure
  src="https://ai.thesatyajit.com/articles/opencut/fig1.png"
  alt="The OpenCut editor in a browser. Left: an assets panel with two imported video thumbnails, clip-a.mp4 (6 s, colour bars) and clip-b.mp4 (4 s, a Mandelbrot fractal). Centre: a preview showing the colour bars at 00:00:01:11 of 00:00:07:01. Right: a properties panel for a text element with Font Arial, Size 15 and Color FFFFFF. Bottom: a timeline with three rows: a green 'Default text' element from 2 s, clip-b.mp4 on an overlay row from 2 s, and clip-a.mp4 on the main row from 0 s. A blue playhead sits near 1.4 s."
  caption="The classic editor at opencut.app with my test project: a 6 s clip on the main track, a 4 s clip on an overlay track and a text element, both starting at 2 s (opencut.app editor, my screenshot)."
/>

### Time is an integer

The first decision is the one most editors get wrong early. `rust/crates/time` defines `MediaTime` as an `i64` count of ticks, with `TICKS_PER_SECOND = 120_000`. A frame rate is a rational `{numerator, denominator}`, and a frame lasts

$$
\text{ticks per frame} = \frac{120{,}000 \times \text{den}}{\text{num}}
$$

The crate's own test fixes the values: 5,005 ticks at 23.976, 5,000 at 24, 4,800 at 25, 4,004 at 29.97, 4,000 at 30, 2,002 at 59.94, 2,000 at 60 and 1,000 at 120 (measured, from `frame_rate.rs`). `ticks_per_frame` returns `None` when the division is not exact, so a rate like 7/3 is rejected rather than rounded.

Why bother? NTSC's 29.97 is really 30000/1001. Frame *n* starts at *n* × 1001/30000 seconds. That number has no exact binary floating-point form, so a float timeline drifts off frame boundaries and two clips that should abut overlap by a sliver. At 4,004 ticks a frame, every frame boundary is an exact integer (reasoned). The changelog for 0.3.0 says the same: floats *"accumulated rounding errors and made frame alignment imprecise."*

### The timeline model

`timeline/types.ts` defines a scene as three fields, not a flat list:

```ts
// apps/web/src/timeline/types.ts (opencut-classic)
export interface SceneTracks {
  overlay: OverlayTrack[];   // video, text, graphic or effect tracks
  main: VideoTrack;          // exactly one
  audio: AudioTrack[];
}
```

The type makes a few mistakes impossible: there is always one main video track, and audio tracks cannot sit between video layers. Every element carries the same four times, all in ticks: `startTime` (where it sits on the timeline), `duration` (how long it occupies), `trimStart` and `trimEnd` (how much of the source is cut from each end). Video and audio may carry `retime: { rate, maintainPitch }`, with the rate clamped to 0.01 to 5 (measured, `retime/rate.ts`). Effects are either attached to a clip or placed as their own elements on an effect track, where they apply to everything beneath them.

### Drawing one frame

To draw time *t*, `scene-builder.ts` turns the scene into a tree of render nodes. The background goes first (a solid colour, or a blurred copy of the main track). The tracks go on top, from the main track upward. Then `resolve.ts` visits each node and does this arithmetic:

$$
\text{clipTime} = t - \text{startTime}, \qquad
\text{sourceTime} = \text{trimStart} + \text{clipTime} \times \text{rate}
$$

If `clipTime` falls outside `[0, duration)`, the node is skipped. Otherwise a video node asks the decoder for the source frame at `sourceTime`. The widget below runs exactly this on a three-element project: drag the playhead, trim clip A's head, change clip B's speed, switch the frame rate, and watch which elements are drawn and which source frame each one requests.

<FrameResolver />

The rest is three subsystems:

1. **Decode.** `services/video-cache` wraps [mediabunny](https://mediabunny.dev)'s `CanvasSink`, which sits on WebCodecs' `VideoDecoder`. It holds the current frame and prefetches the next. A seek bumps a generation counter so a stale decode cannot overwrite a newer one. There is no FFmpeg anywhere in the classic tree (measured, by search).
2. **Composite.** `opencut-wasm` is the Rust workspace compiled with `wasm-bindgen`. Its `gpu` crate asks `wgpu` 29 for a WebGPU adapter and falls back to WebGL2 (`Backends::GL`) when there is none (measured, `gpu/src/context.rs`). The `compositor` crate draws layers with blend modes. `effects` holds one effect shader, a Gaussian blur run as horizontal and vertical passes. `masks` feathers mask edges with a jump-flood distance field. TypeScript decides which passes to run and with which uniforms; Rust owns the device, textures and passes.
3. **Audio.** At export, `createTimelineAudioBuffer` mixes every unmuted audio element into one stereo buffer at 44,100 Hz (measured, `media/audio.ts`). Clips with `maintainPitch` are stretched through an `OfflineAudioContext` with SoundTouch.

### Export: the same loop, written to a file

`scene-exporter.ts` is 171 lines. It computes `frameCount = floor(duration / ticksPerFrame)`, then for each frame calls the same `render()` the preview uses and hands the compositor's canvas to mediabunny's `CanvasSource`. That encodes through WebCodecs' `VideoEncoder`: H.264 for MP4, VP9 for WebM, at one of four quality presets. Audio is AAC for MP4, with a capability check that falls back to Opus when the browser cannot encode AAC. The muxer writes into a `BufferTarget`, so the finished file sits in memory before it is offered as a download. Nothing in that path adds a mark. The repository's only hits for "watermark" are two lines in the terms page promising there is none (measured).

I exported my test project from `opencut.app` with the defaults (MP4, High, audio on). Chromium ran under Xvfb with Mesa's software OpenGL and no GPU, on a shared 16-core machine.

<Figure
  src="https://ai.thesatyajit.com/articles/opencut/fig2.png"
  alt="The same editor with an 'Exporting project' popover open at the top right, showing a progress bar at 7% and a Cancel button. The preview shows the colour-bar clip."
  caption="Export in progress. The popover offers MP4 (H.264) or WebM (VP9), four quality levels and an audio toggle; the bar is the frame loop advancing (opencut.app editor, my screenshot)."
/>

| Exported file | Value | Label |
|---|---|---|
| Container, video codec | MP4, H.264 High | measured |
| Size, frame rate, frames | 1280 × 720, 30 fps, 211 frames | measured |
| Duration | 7.04 s | measured |
| Audio | Opus, 48 kHz, stereo | measured |
| File size | 2,205,500 bytes | measured |
| Wall time, button to download | 184.5 s | measured |
| Per frame | about 0.87 s | reasoned |
| Against real time | about 26 times slower | reasoned |

Four things in that table come straight from the code. 211 frames is the loop's `floor(duration / ticksPerFrame)` at 4,000 ticks a frame, so any trailing partial frame is dropped. The canvas is 1280 × 720, not the 1920 × 1080 default, because `insert-element.ts` resizes the canvas and sets the frame rate to match the first video or image placed on an empty timeline. The audio is Opus inside MP4 because this Chromium build reported no AAC encoder, which is the fallback the exporter checks for. And the frame below, decoded from the file at 3 s, has the text over clip B over clip A, with no watermark on it or on the last frame.

<Figure
  src="https://ai.thesatyajit.com/articles/opencut/fig4.jpg"
  alt="A 1280 by 720 video frame: a Mandelbrot fractal in green, red and orange on a pink background, with the white words 'Default text' centred over it. There is no logo or watermark in any corner."
  caption="Frame at 3.0 s of the exported MP4: the text element composited over clip B, which covers clip A. No mark is added (my export from opencut.app)."
/>

Do not read 26 times slower than real time as OpenCut's speed. My run had no GPU: the wgpu compositor ran on software OpenGL and the encoder in software. With hardware decode, encode and a real GPU it will be much faster, and I did not measure that. What the run does show is the shape of the loop. Each frame waits for the previous one to resolve, decode, composite and encode, and the whole file lives in memory until the end. A first attempt in plain headless Chromium with SwiftShader did not get that far. Adding a clip to the timeline panicked the WebAssembly compositor (`RefCell already borrowed`, `compositor.rs`) and took the page down (measured). That is my environment, not a bug report, but it shows how much of the editor rests on the GPU path.

## Where the projects live

`services/storage` keeps project documents in IndexedDB (`video-editor-projects`). Imported media is copied into the Origin Private File System, one directory per project (`media-files-<projectId>`), with metadata in a per-project IndexedDB. The editor calls `navigator.storage.persist()` so the browser will not evict it under disk pressure; Firefox gets a dialog first. The schema has gone through 31 versions, each with a migration (measured, `v30-to-v31.ts` is the latest).

That is the private part of "your videos stay on your device", and it is real: media never leaves the browser. It is also the answer to the reply about six-month-old projects. I found no project file to save or open: the only `exportProject` in the tree renders video (measured, by search). A project lives in one browser profile, on one origin. Clear site data, or switch machines, and it is gone. Moving to the rewrite will mean moving out of `opencut.app`'s storage, and nothing in either repository describes how yet.

## What the AI is

The post says "AI agents". In the code that ships, the AI is captioning. `services/transcription/worker.ts` runs Whisper in a Web Worker through `@huggingface/transformers` (transformers.js), with four ONNX checkpoints from `onnx-community`: tiny, small, medium and large-v3-turbo, `small` by default (measured, `transcription/models.ts`). It loads weights quantised to 4 bits (`dtype: "q4"`) and lets the library pick WebGPU or WASM. Audio goes in at 16 kHz in 30 s chunks with a 5 s stride, and the timestamped chunks become caption elements. Like the media, it all runs in the browser.

The agent story is the rewrite's MCP server, and that server does not exist yet. The README lists fal.ai as a sponsor, a generative image, video and audio API. Neither tree calls it: the classic repository mentions fal only in its sponsor list (measured). If you want models making video, see [Kandinsky 6.0 Video](/articles/kandinsky-6-video). If you want agents producing it, [one-shot launch videos](/articles/one-shot-launch-videos) and [fframes](/articles/fframes), a code-first renderer that links libav directly, are both closer to that today.

## The claims, checked

| Claim in the post | Verdict |
|---|---|
| Free | Holds. Both repositories are MIT (measured, `LICENSE`). |
| No watermarks | Holds. No mark in the exporter, none in my exported file (measured). |
| Multitrack editing | Holds for the classic editor: a main track plus any number of overlay and audio tracks. The rewrite has no timeline. |
| Plugins | Roadmap. The rewrite's README lists them as coming; no plugin API exists in either tree. |
| AI agents | Roadmap (an MCP server). Shipped AI today is Whisper captioning in the browser. |
| Automations | Roadmap (headless mode, a scripting tab). |
| Cross-platform | Partly. The classic editor is a web app, so it runs wherever a browser with WebCodecs runs. The desktop app is a window; mobile has no code. |
| 92K GitHub stars | Holds as a number: 92.9K (reported). They belong to a repository that holds the rewrite's scaffold, not the editor. |

## What I would tell someone choosing it today

The classic editor is a serious piece of browser engineering. It has integer time, a typed track model, a Rust compositor that degrades from WebGPU to WebGL2, and WebCodecs decode and encode with no server in the loop. It also makes the trade every in-browser editor makes. Your project lives in browser storage. Export runs one frame at a time on whatever GPU the tab gets. The repository was archived in May, so the bugs it has now are the bugs it keeps.

The rewrite is a bet on a different shape: a Rust core with FFmpeg underneath, plugins and an agent-facing API from the start, and the same engine headless. That is the right shape for "AI agents and automations" if it lands. Today it is a README.

That is the gap the post fell into, and it is a common one. A repository's README describes the project's future. A post summarises the README. A reader assumes it describes the present. For anything that goes viral on star count, check which commit the stars are attached to. [The TikTok dataset piece](/articles/tiktok-5-6b-videos) found the same thing from the other direction: the card is a claim, and the files are the evidence. For more on what WebGPU and WebAssembly can carry in a tab, see [GTA V in a browser](/articles/gta5-in-the-browser) and [Jev in the browser](/articles/jev-in-the-browser).
