Cut local. Stay private. Go viral.
A local-first, offline video editor for creators making short-form content. Every byte stays on your machine.
Snipette is a desktop video editor aimed at TikTok, Reels, and Shorts creators who would rather not hand their footage to someone else's cloud. It runs entirely on your machine: no accounts, no telemetry, no upload step. Think of it as a CapCut alternative that respects your hard drive and your privacy.
- Screenshots
- Features
- Tech Stack
- Requirements
- Quick Start
- Project Structure
- Scripts
- Whisper Setup (Local Captions)
- Architecture
- Build & Package
- Privacy
- License
- Multi-track timeline with razor, slip, snap, and per-edge resize
- Keyframeable transforms, opacity, color, and audio fx
- 28+ named text animation templates (block-reveal, kinetic typography, drop-in subtitle, etc.)
- Beat detection and audio ducking
- Voice recording and local TTS
- Thumbnail strip generation via ffmpeg
- WebCodecs based export pipeline plus an ffmpeg fallback
- Local speech-to-text with whisper.cpp (English models, word-level timestamps)
- Auto-backup with restore points stored next to your project
| Package | Version | Purpose |
|---|---|---|
electron |
^32.1.2 |
Desktop shell |
react |
^18.3.1 |
Renderer UI |
react-router-dom |
^6.27.0 |
Client-side routing |
zustand |
^4.5.5 |
Global state |
framer-motion |
^11.11.0 |
Motion and gesture |
@react-spring/web |
^9.7.4 |
Spring animation for text fx |
better-sqlite3 |
^11.3.0 |
Local project DB |
electron-store |
^10.0.0 |
User settings (JSON) |
electron-log |
^5.2.0 |
File and console logging |
ffmpeg-static |
^5.2.0 |
Bundled ffmpeg binary |
ffprobe-static |
^3.1.0 |
Bundled ffprobe binary |
mp4-muxer |
^5.2.2 |
WebCodecs MP4 muxing |
date-fns |
^3.6.0 |
Date utilities |
uuid |
^10.0.0 |
ID generation |
| Package | Version | Purpose |
|---|---|---|
typescript |
^5.6.2 |
Static typing |
vite |
^5.4.8 |
Dev server and bundler |
electron-vite |
^2.3.0 |
Electron-aware Vite preset |
electron-builder |
^25.1.7 |
Packaging and native rebuild |
tailwindcss |
^3.4.13 |
Styling |
postcss + autoprefixer |
^8.4.47 / ^10.4.20 |
CSS pipeline |
prettier |
^3.3.3 |
Formatting |
| Tool | Where it lives | Used by |
|---|---|---|
ffmpeg / ffprobe |
Pulled in by ffmpeg-static and ffprobe-static, unpacked from asar at runtime |
electron/services/ffmpeg.service.ts, encoder.ts |
whisper.cpp CLI plus model |
Manually placed in resources/ |
electron/services/whisper.service.ts |
- Node 18.18+ (Node 20 LTS recommended). Node 26 currently breaks the pinned
better-sqlite3@^11.3.0, so stick to 20 unless you bump the dep. - npm 9+ (or pnpm/yarn, scripts assume npm)
- Python 3 on
PATHfornode-gypto compilebetter-sqlite3against Electron headers - Xcode Command Line Tools on macOS, build-essential on Linux, windows-build-tools or VS Build Tools on Windows
- Optional: a built
whisper.cppfor local captions, see Whisper Setup
git clone https://github.com/your-org/snipette.git
cd snipette
npm install
npm run devThe postinstall hook does two things:
- Runs
ffmpeg-static's install script, which downloads the platform-specific ffmpeg binary intonode_modules/ffmpeg-static/. - Runs
electron-builder install-app-deps, which rebuildsbetter-sqlite3against Electron's ABI (your system Node ABI will not match).
If npm install fails compiling better-sqlite3:
npm install --ignore-scripts
npm run rebuildsnipette/
├── electron/ # Main process
│ ├── main.ts # Entry: BrowserWindow, app lifecycle
│ ├── preload.ts # contextBridge: typed IPC surface
│ ├── ipc/ # One file per IPC channel group
│ │ ├── project.ipc.ts
│ │ ├── media.ipc.ts
│ │ ├── timeline.ipc.ts
│ │ ├── export.ipc.ts
│ │ ├── web-export.ipc.ts
│ │ ├── whisper.ipc.ts
│ │ ├── tts.ipc.ts
│ │ ├── voice-recording.ipc.ts
│ │ ├── backup.ipc.ts
│ │ ├── settings.ipc.ts
│ │ └── system.ipc.ts
│ └── services/ # Pure node-side logic
│ ├── db.service.ts # better-sqlite3 wrapper
│ ├── ffmpeg.service.ts # ffmpeg orchestration
│ ├── encoder.ts # WebCodecs export pipeline
│ ├── whisper.service.ts # whisper.cpp + WAV extraction
│ ├── tts.service.ts
│ ├── thumbnail.service.ts
│ ├── beat-detect.service.ts
│ ├── audio-graph.ts
│ ├── audio-ducking.ts
│ ├── audio-fx-filter.ts
│ ├── video-fx-filter.ts
│ ├── filter-graph.ts
│ ├── keyframe-filter.ts
│ └── backup.service.ts
│
├── src/ # Renderer (React)
│ ├── main.tsx # ReactDOM entry
│ ├── App.tsx # Router root
│ ├── routes/ # Route components
│ ├── components/ # UI components (timeline, inspector, preview, ...)
│ ├── store/ # Zustand stores
│ ├── hooks/ # Reusable hooks
│ ├── utils/ # Renderer-side helpers
│ ├── types/ # Renderer-only types
│ └── styles/ # Tailwind entry, global CSS
│
├── shared/
│ └── types.ts # Types shared across main + renderer (IPC contracts)
│
├── resources/ # Runtime binaries (whisper, models, etc.)
├── assets/ # Bundled fonts, LUTs, sounds (shipped in build)
├── out/ # electron-vite build output
└── release/ # electron-builder packaged artifacts
npm run dev # Electron + Vite dev server with HMR
npm run build # Production bundle into out/
npm run build:check # tsc --noEmit, then electron-vite build
npm run typecheck # tsc --noEmit across main and renderer projects
npm run preview # Preview the production build
npm run start # electron . against an existing build in out/
npm run rebuild # Re-run electron-builder install-app-deps
npm run format # prettier --write across the repoCaptions run through whisper.cpp on your own CPU/GPU. The Captions menu lights up automatically once both the binary and the model are present at the paths Snipette expects.
electron/services/whisper.service.ts looks for:
| File | Path (dev) | Path (packaged) |
|---|---|---|
| Binary | resources/whisper (or resources\whisper.exe) |
process.resourcesPath/whisper/whisper |
| Model | resources/ggml-base.en.bin |
process.resourcesPath/whisper/ggml-base.en.bin |
The model filename is currently hardcoded to
ggml-base.en.bin. If you want a different size, rename your download to match.
git clone https://github.com/ggerganov/whisper.cpp.git
cd whisper.cpp
cmake -B build
cmake --build build --config Release -jThe CLI ends up at build/bin/whisper-cli. Copy it into Snipette and rename:
cp build/bin/whisper-cli /path/to/snipette/resources/whisperwhisper.cpp ships as a CLI plus a handful of shared libs. They must live next to the binary so @loader_path resolves correctly:
resources/
├── whisper # CLI binary (renamed from whisper-cli)
├── ggml-base.en.bin # English model, ~148 MB
├── libwhisper.1.dylib
├── libggml.0.dylib
├── libggml-base.0.dylib
├── libggml-cpu.0.dylib
├── libggml-blas.0.dylib
└── libggml-metal.0.dylib # Metal backend, GPU acceleration on Apple Silicon
Grab the dylibs from your build at build/src/ and build/ggml/src/. If the binary still complains about /tmp style paths, the rpaths got hardcoded during the build. Patch them and re-sign:
cd resources
install_name_tool -change /tmp/build/.../libwhisper.1.dylib @loader_path/libwhisper.1.dylib whisper
# repeat for each dylib referenced by `otool -L whisper`
codesign --force --sign - whisper *.dylibOn Linux the equivalent files are .so libraries, drop them next to whisper and chmod +x the binary. On Windows ship the .dll files next to whisper.exe.
| File | Size | Recommendation |
|---|---|---|
ggml-tiny.en.bin |
~75 MB | Fastest, lowest accuracy |
ggml-base.en.bin |
~148 MB | Default, balanced |
ggml-small.en.bin |
~466 MB | Better accuracy, still real-time on Apple Silicon |
ggml-medium.en.bin |
~1.5 GB | Highest quality English-only model |
Pull from Hugging Face:
curl -L -o resources/ggml-base.en.bin \
https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.binFor non-English content, swap to the multilingual variants (ggml-base.bin, ggml-small.bin, ggml-large-v3.bin) and rename to ggml-base.en.bin. Accuracy on English will drop slightly compared to the .en models.
resources/whisper -m resources/ggml-base.en.bin -f path/to/audio.wav --output-json-fullA successful run prints a transcript and writes a *.json file with word-level timestamps. If you get dyld: Library not loaded, the dylibs are missing or unsigned. If you get failed to load model, the model file is corrupt or wrong size.
+--------------------+ contextBridge +--------------------+
| | <-- typed IPC (preload.ts) -->| |
| Renderer (React) | | Main (Node) |
| | | |
| - Zustand stores | | - IPC handlers |
| - Components | | - DB service |
| - Routes | | - ffmpeg / encoder|
| | | - whisper / tts |
+--------------------+ +--------------------+
| |
| shared/types.ts (IPC contracts, project schema) |
+-------------------------------------------------------+
contextIsolationis on,nodeIntegrationis off. The renderer has no Node access.- Every IPC channel is typed in
shared/types.tsand surfaced throughelectron/preload.ts. - Long-running tasks (export, transcription, beat detection) stream progress events back to the renderer via the IPC bridge.
- Project state is persisted to a
better-sqlite3database underapp.getPath('userData').
npm run build # bundle main + renderer
npx electron-builder --mac # produce a .dmg / .app
npx electron-builder --win # produce an NSIS installer
npx electron-builder --linux # produce an AppImageNative deps that must be unpacked from asar at runtime are declared in package.json under build.asarUnpack:
"asarUnpack": [
"node_modules/ffmpeg-static/**",
"node_modules/ffprobe-static/**",
"node_modules/better-sqlite3/**"
]Output lands in release/.
- No network calls in the hot path. No telemetry, no analytics, no accounts.
- All project data lives in
app.getPath('userData')on your machine. - Update checks are opt-in and off by default.
Released under the MIT License. Copyright (c) 2026 Joppe Montezinos.





