Short-form institutional video — record and edit on-device, upload to your own server.
Capture knowledge before it walks out the door — training walkthroughs, process demos, handoffs — without sending a single frame to a third-party cloud.
Pulse is a React Native (Expo) app for capturing institutional knowledge as short-form video. Everything happens on the device: segmented recording, trimming, merging, and even speech-to-text captioning run locally. When you're ready to publish, Pulse uploads to a PulseVault server you run — your organization keeps the content on its own infrastructure and owns auth, retention, and quota decisions. No central Pulse service exists.
Two words show up throughout Pulse — here's what they mean.
A Pulse is a single short-form video, assembled on-device from an ordered set of clips. Record a few, import a few, reorder them — on export they merge (losslessly where formats match) into one .mp4. Publishing a Pulse sends a small bundle: the merged video, its captions (WebVTT), a beat manifest, and a thumbnail. On-device each Pulse is a draft in your library; drafts move between devices as .pulse bundles.
flowchart LR
subgraph pulse["🫀 One Pulse — an ordered set of clips"]
direction LR
c0["Clip 0"] --> c1["Clip 1"] --> c2["Clip 2"]
end
pulse ==>|"merge on export"| merged["Merged .mp4"]
merged --> vtt["Captions · .vtt"]
merged --> man["Beat manifest"]
merged --> thumb["Thumbnail"]
A Beat is one clip's exact slot on the merged timeline — { segmentId, order, startMs, endMs }. The beat manifest records every clip's start/end so any moment in the merged video can be traced back to the segment it came from (and later deep-linked — e.g. HLS chapter jumps). Beats are contiguous — each beat's endMs equals the next beat's startMs — and sum exactly to the Pulse's true duration.
flowchart TB
subgraph clips["Recorded clips, in order"]
direction LR
s0["Clip 0"] --> s1["Clip 1"] --> s2["Clip 2"]
end
subgraph tl["One merged timeline · 0 → durationMs"]
direction LR
b0["Beat 0<br/>0 – 4200 ms"] --- b1["Beat 1<br/>4200 – 9100 ms"] --- b2["Beat 2<br/>9100 – 15000 ms"]
end
s0 -.-> b0
s1 -.-> b1
s2 -.-> b2
- 🎬 Segmented recording — build a walkthrough from multiple clips, reorder them, re-record the ones you fumbled
- ✂️ Non-compounding edits — trims re-encode from the pristine original every time, so re-editing never stacks generation loss
- 🗣️ On-device captions — Whisper (whisper.cpp) transcription with word-level timing, no audio ever leaves the phone
- 📡 Resumable uploads — TUS v1 protocol; a two-minute capture survives signal drops, app kills, and relaunches
- 🔐 Self-hosted by design — pair with your server via QR / deep link; capability tokens live in the device keychain
- 📦 Local-first drafts — SQLite-backed library that works fully offline, shareable device-to-device as
.pulsebundles
- Multi-clip segmented recording with VisionCamera v5 — segments append to one draft with a live segment bar and drag-to-reorder
- Front/back flip, torch, mic mute, tap-to-focus, pinch-to-zoom, lens selection, and stabilization modes (off / standard / cinematic / auto), all persisted across sessions
- Call-aware capture: a custom native module (CallKit on iOS) gates the microphone during phone/VoIP calls so recordings don't silently freeze
- Import existing videos from Photos instead of recording
- Per-clip trim and transform via FFmpeg — destructive but safe: re-editing always reopens the untouched original
- Smart merge on export: clips with matching formats are concatenated losslessly (passthrough); mixed resolution/fps/codec/orientation drafts conform only the outlier clips, with a full re-encode fallback
- Save to Photos or Files, share anywhere, or upload — with a live progress ring
- Fully on-device speech-to-text with whisper.rn — choose Base (en), Small (en), Small multilingual, or Large-v3-turbo, downloaded on demand
- Silero VAD pre-gate skips silent clips and suppresses Whisper's hallucinated filler on empty audio
- Word-level timestamps reflowed into broadcast-style cues (line-length, duration, and sentence-boundary aware)
- Optimistic caption editor with autosave and undoable edit history — hand-edited cues are protected from being overwritten by re-transcription or model switches
- Captions upload as WebVTT with word-level cue timestamps (karaoke-ready) alongside the video
- Local-first draft library on Drizzle ORM + SQLite — create, rename, delete, with live-updating thumbnails
- Device-to-device draft sharing as
.pulsebundles (zip + manifest, with unpack safety limits) - First-run onboarding tour
- Pair with a server by scanning a QR / opening a
pulsecam://deep link — trust-on-first-use confirmation, capability negotiation against the server's/capabilitiesendpoint - TUS v1 resumable uploads with exponential backoff; interrupted uploads resume from the server's true byte offset, even after an app relaunch
- Two upload strategies, negotiated per server: merged (one video + captions + beat-timecode manifest + thumbnail) or segment (per-segment clips + an ordering manifest)
- Bearer tokens stored in the secure keychain, never in the database
┌─────────────────────────── on device ───────────────────────────┐
│ │
│ Record (VisionCamera) ─▶ Trim/Merge (FFmpeg) ─▶ Draft (SQLite) │
│ │ │
│ Whisper + VAD ─▶ captions (VTT) │
│ │
└──────────────────────────────┬───────────────────────────────────┘
│ pair via pulsecam:// QR / deep link
▼
TUS v1 resumable upload (video + VTT)
│
▼
┌──────────────────────────────┐
│ PulseVault (self-hosted) │
│ your auth · your storage │
└──────────────────────────────┘
The server side lives in pulsevault-mieweb/: a Fastify plugin (with a framework-agnostic core for Express, Meteor, or plain Node http) that receives Pulse uploads into filesystem-first storage, with authorize / validatePayload / onUploadComplete hooks for wiring in your SSO, audit trail, transcoding, or AI pipeline. The wire contract is documented in PROTOCOL.md — an implementation-independent spec (capability discovery, artifact kinds, tokens, TUS) so anyone can build a compatible server.
Git LFS required. The dev-seed video fixtures in
assets/dev/*.mp4are stored in Git LFS. Install it before cloning (or pull after), otherwise those files arrive as tiny pointer stubs and the dev+ seedbutton breaks:brew install git-lfs && git lfs install # one-time git lfs pull # if you cloned before installingThe ~400 MB fixture-regen master (
fixtures/bbb_master.mov) is intentionally excluded from normal clones (see.lfsconfig); fetch it only when regenerating fixtures:git lfs pull --include "fixtures/*.mov".
# 1. Install dependencies
npm install
# 2. Run a development build (native modules — Expo Go won't work)
npm run ios # or: npm run androidPulse uses native modules (VisionCamera, Whisper, FFmpeg), so it needs a dev build, not Expo Go.
No camera? No problem. In a dev build, the Home screen has + seed / clear buttons that create a "Dev sample" draft from the bundled assets/dev/ clips — including deliberately mismatched resolution/fps/codec/orientation clips that exercise the export-normalization path — so the full editor is drivable on a simulator. See assets/dev/README.md.
| Command | What it does |
|---|---|
npm start |
Start the Metro dev server |
npm run ios / npm run android |
Build & run the dev client |
npm test |
Run the Jest unit-test suite |
npm run lint |
ESLint via expo lint |
npm run format |
Prettier |
src/
├── app/ # expo-router screens: home, recorder, subtitles, export, onboarding
├── features/ # recorder, transcription, export, upload, draft-transfer, home, …
├── db/ # Drizzle ORM schema + queries (expo-sqlite)
├── components/ # shared themed UI
└── dev/ # __DEV__-only seed tooling (dead-code-eliminated in release)
modules/ # custom native modules (expo-call-detector)
plugins/ # Expo config plugins
pulsevault-mieweb/ # self-hosted upload server + protocol spec
fixtures/ # fixture-regen sources (Big Buck Bunny, CC-BY)
Unit tests are co-located with the code and scoped to pure logic — cue reflow, upload/TUS state machines, deep-link parsing, edit history, autosave gating — so they run in a plain Node environment with no native rendering. PulseVault ships its own suite covering the HTTP adapters, capability tokens, and checksum handling.
npm testSee LICENSE.
Made with ❤️ at MIE — built on React Native, Expo, whisper.cpp, and FFmpeg.