Skip to content
miewebPublic

About

Short-form institutional video — record and edit on-device, upload to your own server. Pulse-Camera App on iOS AppStore & Andriod Playstore

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

800 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Pulse app icon

Pulse

Short-form institutional video — record and edit on-device, upload to your own server.

React Native Expo TypeScript Whisper VisionCamera

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.

Anatomy of a Pulse

Two words show up throughout Pulse — here's what they mean.

What is a Pulse?

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"]
Loading

What is a Beat?

A Beat is a timestamp range inside the Pulse's one video: where one recorded clip starts and ends — { segmentId, order, startMs, endMs }. The beat manifest records every clip's start/end so any moment in the 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 video · 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
Loading

Highlights

  • 🎬 Segmented recording — build a walkthrough from multiple clips, reorder them, re-record the ones you fumbled
  • ✂️ Non-compounding edits — trims, crops, rotations and speed are stored as settings over the pristine original and rendered once at export, 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 .pulse bundles

Features

Recording & camera

  • 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 levels (off / on / high / auto), all persisted across sessions
  • Call-aware capture: a custom native module (audio-interruption detection on iOS, audio-mode detection on Android) gates the microphone when the audio session is interrupted so recordings don't silently freeze
  • Import existing videos from Photos instead of recording

Editing & export

  • 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

Captions & transcription

  • Fully on-device speech-to-text with whisper.rn — Parakeet for English, or a multilingual Whisper (Large Turbo on iOS, Small on Android), downloaded from the editor's Captions tool
  • Each clip is transcribed once, in the background; the draft's captions are composed from the clips through their trims, speed, mutes and order, so an edit never re-runs the model
  • 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; Chinese, Japanese and Thai keep their per-character timing), never across a cut
  • Captions live in the editor: shown over the preview and on a lane under the clips; tap a word to edit or delete it, split or merge sentences at the playhead, drag the caption to move them all — edits are kept per clip and undoable
  • Captions upload as WebVTT with word-level cue timestamps (karaoke-ready) and their position, alongside the video

Drafts & library

  • Local-first draft library on Drizzle ORM + SQLite — create, rename, delete, with live-updating thumbnails
  • Device-to-device draft sharing as .pulse bundles (zip + manifest, with unpack safety limits)
  • First-run onboarding tour

Upload & pairing

  • Pair with a server by scanning a QR / opening a pulsecam:// deep link — trust-on-first-use confirmation, capability negotiation against the server's /capabilities endpoint
  • TUS v1 uploads that retry with exponential backoff, resuming from the server's true byte offset; they carry on in the background, and a link is used once — if an upload can't finish (or the app is killed), it's cleaned up and a new link is the retry
  • A link can say which place in the host app its upload is for — a ticket, a team's update (protocol 2.7). Pulse then opens the recorder on the draft already started for that place, or on a new one named by the link, preselects the place when you upload, and offers the way back to the host once the video is in. The recorder opens once the server has answered (a toast says it is being asked, a few seconds at most), so a first link needs a connection; a link the place already holds opens its recorder at once. Until a draft uses it, a place has one link on the device: a newer one the server accepted replaces it, an expired one is refused. The link is still one upload — after a failed or cancelled one, a new link for the place comes back to the same draft
  • Which place a link is for is read from its token, which Pulse can't verify: only a link the server has accepted replaces the one a place already has. What a link calls the place, the recording and the host app is unsigned text: shown as one clean line, never deciding where a video goes, and always beside the server's host. The way back opens only on a tap, and its button names the host it leads to
  • A Pulse uploads as one video, plus its captions (WebVTT), beat manifest and thumbnail — each declaring relatedTo the video
  • A finished upload's Watch and Copy link live in the draft's ⋯ menu on Home. Copy link shares a read-only view link (servers on protocol 2.2+), never the upload token
  • Bearer tokens stored in the secure keychain, never in the database

Platform support

Pulse runs on iOS and Android from a single codebase. iOS is the original platform; Android has been brought to parity and verified end-to-end on device — recording, editing, merge/export, on-device captions, .pulse sharing, pairing, and resumable uploads (including upload survival through backgrounding and Doze via a foreground service).

Platform notes:

  • High (cinematic) stabilization is iOS-only (VisionCamera exposes it via AVFoundation); Android offers off / on / auto
  • Whisper transcription uses Metal GPU acceleration on iOS; Android runs on CPU, so larger models are noticeably slower — Base (en) is the recommended starting point
  • Call-aware capture uses AVAudioSession interruptions on iOS (no CallKit — it must stay out of the binary for China App Store distribution) and an audio-mode heuristic on Android (no extra permissions), so both platforms pause the mic when the session is interrupted
  • On Android, merged exports save through MediaStore into DCIM/, and uploads run as a dataSync foreground service with a progress notification

How it works

┌─────────────────────────── 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    │
                └──────────────────────────────┘

PulseVault — self-hosted uploads

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.

Getting started

Git LFS required. The dev-seed video fixtures in assets/dev/*.mp4 are stored in Git LFS. Install it before cloning (or pull after), otherwise those files arrive as tiny pointer stubs and the dev + seed button breaks:

brew install git-lfs && git lfs install   # one-time
git lfs pull                              # if you cloned before installing

The ~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".

Requires Node.js ≥ 24.3.0 (the engines field in package.json; CI uses Node 24).

# 1. Clone with the PulseVault submodule (or, in an existing clone: git submodule update --init)
git clone --recurse-submodules https://github.com/mieweb/pulse.git && cd pulse

# 2. Install dependencies
npm install

# 3. Run a development build (native modules — Expo Go won't work)
npm run ios       # or: npm run android

Pulse uses native modules (VisionCamera, Whisper, FFmpeg), so it needs a dev build, not Expo Go.

The pulsevault-mieweb/ submodule pins the server this app is built against. It holds the protocol JSON Schemas that src/features/upload/protocol.gen.ts is generated from, and it is the server used by the integration suite (see Testing).

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.

Development

Commands

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
npx tsc --noEmit Typecheck
npm run format / npm run format:check Prettier (write / check only)
node scripts/gen-protocol-types.mjs Regenerate src/features/upload/protocol.gen.ts from the submodule's schemas and package.json pulseProtocol (--check fails if it's stale)

CI (.github/workflows/ci.yml) runs the typecheck, lint, gen-protocol-types.mjs --check, and unit tests on every PR, plus the integration suite against both the pinned PulseVault and the latest npm release.

Project structure

src/
├── app/            # expo-router screens: home, recorder, editor, 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)

Testing

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 test

A heavier suite is opt-in and skips itself in a plain npm test:

# Cross-repo integration: the app's real pairing + upload code against a real PulseVault
# server built from the submodule (set PV_CORE=<path to core.js> to test another build)
(cd pulsevault-mieweb && npm ci && npm run build)
PULSE_INTEGRATION=1 npx jest pv-integration --forceExit

License

See LICENSE.


Made with ❤️ at MIE — built on React Native, Expo, whisper.cpp, and FFmpeg.

About

Short-form institutional video — record and edit on-device, upload to your own server. Pulse-Camera App on iOS AppStore & Andriod Playstore

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages