Convert, speed-ramp, crop-and-pan, mute, score, or extract audio — without memorizing a single flag, and without a single frame of your footage ever leaving your machine.
- Why local
- What It Does
- Quick Start
- Requirements
- Architecture
- Project Structure
- Scripts
- Change Speed, in Detail
- Production Build
- Design System
- Roadmap
- License
Every operation here has a web app that does the same thing — for a price. You upload your footage to someone else's server, wait in a queue, and hope the free tier doesn't watermark it. VideoForge skips all of that: your video never leaves your disk. ffmpeg does the work on your own CPU, in your own filesystem, with no account, no upload progress bar, and no limit on file size or minutes per month.
Pick a clip. Pick an operation. The right controls appear — nothing else.
Every operation runs in batch — select ten clips, pick one destination folder, and ten correctly-named outputs come out the other side. (Add Music is the one exception: it needs exactly one video and one track, by nature.)
Import a clip and its real duration, resolution, and audio presence show up immediately — read straight off ffprobe, not guessed.
Slider, presets, or an exact number — whichever's faster for you. |
Pick a ratio, pick a direction, done. |
This is a real export, not a mockup — a 65KB test clip muted down to 28KB, with the actual output path shown underneath.
npm install
npm run devA window opens on its own. That's the app — not a browser tab. If you're tempted to check localhost:5173 in Chrome instead: don't. That address is just how Vite talks to the Electron window internally; opening it yourself gets you the interface with none of the filesystem or ffmpeg access wired in, since only the Electron window has that bridge attached.
| Requirement | Notes |
|---|---|
| Node.js | 18+ |
| ffmpeg + ffprobe | On your PATH — the header's status pill tells you immediately if either is missing |
Installing ffmpeg
| OS | Command |
|---|---|
| macOS | brew install ffmpeg |
| Ubuntu / Debian | sudo apt install ffmpeg |
| Windows | winget install ffmpeg |
The interface never touches your disk directly — contextIsolation and sandbox are both on. Every request crosses through one typed bridge, window.api, and on the other side, each concern has exactly one owner:
| Concern | Owner | Why it's singular |
|---|---|---|
| Naming an output file | OutputResolver |
One "Save As" dialog and one batch-export-to-folder flow, both resolving through the same collision-safe naming rule — never two conventions fighting each other. |
| Running ffmpeg | JobQueueManager |
Caps concurrency at 2 jobs, queues the rest, owns cancellation, and is the only thing in the app allowed to emit progress. |
| Building ffmpeg arguments | One FfmpegJob subclass per operation |
A 7th operation means one new class implementing buildArgs() — everything else in the app stays exactly as it is. |
src/
├─ shared/ # The one contract both processes agree on
│ ├─ types.ts Every IPC payload, fully typed
│ └─ operationCatalog.ts Operation metadata: title, suffix, extension, batch support
│
├─ main/ # Node process — the only code touching
│ │ # fs, child_process, or native dialogs
│ ├─ main.ts Thin IPC layer, wires dialogs to services
│ ├─ preload.ts contextBridge — the only surface the UI sees
│ ├─ ffmpeg/
│ │ ├─ FfmpegJob.ts Abstract base: spawn, parse -progress, resolve
│ │ ├─ ConvertVideoJob.ts ┐
│ │ ├─ ExtractAudioJob.ts │ One class per operation, each ported 1:1
│ │ ├─ MuteVideoJob.ts │ from real ffmpeg command-line usage
│ │ ├─ PanCropJob.ts │
│ │ ├─ MergeMusicJob.ts │
│ │ ├─ ChangeSpeedJob.ts ┘ atempo-chained, pitch-preserving speed control
│ │ ├─ JobFactory.ts Maps an operation kind → its Job class
│ │ └─ probe.ts ffprobe wrapper
│ └─ services/
│ ├─ OutputResolver.ts The one place output paths get decided
│ └─ JobQueueManager.ts The one place ffmpeg jobs get run
│
└─ renderer/ # React UI — talks to window.api, nothing else
├─ state/
│ ├─ Store.ts Small observable-snapshot base class
│ ├─ ClipLibrary.ts Imported clips + selection, one source of truth
│ └─ JobQueueStore.ts Mirrors JobQueueManager on the UI side
└─ components/
├─ ClipBin.tsx Left — import, list, select
├─ OperationPanel.tsx Center — pick an operation, configure, run
└─ JobQueue.tsx Bottom — live progress, cancel, results
| Command | What it does |
|---|---|
npm run dev |
The full dev app — Vite, a backend TypeScript watcher, and the Electron window, together |
npm run build |
Production build of the renderer bundle and the compiled main/preload process |
npm start |
Runs the production build |
npm run package |
Builds platform installers via electron-builder, fetched on demand |
The playback-rate control is a percent slider (10%–400%) with quick presets and an exact-value field — hand it 1x, 2x, 10%, 80%, whatever's fastest to type.
- Video —
setpts=(1/rate)*PTSscales presentation timestamps directly. - Audio, pitch-preserved (default) — ffmpeg's
atempofilter only accepts 0.5–2.0 per stage, so anything outside that range is reached by chaining multipleatempostages that multiply out to the target rate (4× becomesatempo=2.0,atempo=2.0) — the standard way past ffmpeg's single-stage ceiling without pitch artifacts. - Audio, pitch not preserved (toggle) —
asetrate+aresample, for the classic tape speed-up/slow-down effect.
npm run build
npm run packageWhy
electron-builderisn't a listed dependency: every current Electron packaging tool —electron-builder, and even the officially-recommended@electron-forge/cli— pulls in old transitive packages through@electron/getinternals that haven't been modernized upstream. Since a packager is only needed when cutting an installer, it's fetched on demand vianpxthe momentnpm run packageactually runs — keepingnpm installitself at zero deprecation warnings.
VideoForge's palette, type, radius, and motion are pulled directly from hassanireza.github.io's real design tokens — not a generic dark theme with a new coat of paint.
| Token | Value | Source |
|---|---|---|
| Background | #08090b / #0d1013 / #12161a |
Exact --bg / --bg-2 / --bg-3 |
| Text | #e6e3da / #9aa3a8 / #7f8589 |
Exact --text / --text-2 / --text-3 |
| Accent | #7c8891 → #c9cfd2 on hover |
Exact --accent / --accent-bright |
| Display type | Cormorant Garamond, italic for emphasis | Exact --font-display |
| Body type | Jost, uppercase + wide tracking for labels | Exact --font-body |
| Radius | 2–3px on cards and buttons; 99px reserved only for floating chrome | Same "sharp everywhere, pill only for chrome" rule |
| Borders | Hairline rgba(214,219,222,.07), brightening to .18 on hover |
Same opacity-only border system |
| Buttons | Idle outline → hover inverts to solid, letter-spacing widens | Same motion as the source site's .submit-btn |
A few identity details carried over on purpose: the wordmark splits roman + italic serif (Video / Forge) the way the source splits a first and last name; operation cards are numbered with counter(op, decimal-leading-zero), the same technique behind their project cards; the six-operation grid uses a 1px hairline-as-grid-gap trick straight out of their palette and mark-grid diagrams.
This is a correctly-architected local tool. Taking it further than your own machine would still want:
- Code signing & auto-update (
electron-buildersupports both — needs your signing certs) - Bundling ffmpeg itself (
ffmpeg-static) instead of requiring it onPATH - Automated tests — the ffmpeg argument-building logic in each
*Job.tsis pure and easy to unit test - Real thumbnail generation in the media bin (currently a placeholder icon)
- Crash/telemetry reporting
Released under the MIT License.



