Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm

- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Typecheck (web)
run: |
# next-env.d.ts is gitignored; generate it (and route types) before tsc.
npx next typegen
npx tsc --noEmit

- name: Typecheck (electron)
run: npm run typecheck:electron

- name: Build Next.js
run: npm run build

- name: Build Electron main/preload
run: npm run build:electron
115 changes: 115 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
name: Build and Release

on:
push:
tags:
- "v*.*.*"

permissions:
contents: write

jobs:
release:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm

- name: Install dependencies
run: npm ci

- name: Typecheck
run: |
npx next typegen
npx tsc --noEmit
npm run typecheck:electron

# macOS: decode the App Store Connect API key used for notarization.
- name: Setup Apple API key
if: matrix.os == 'macos-latest'
run: |
mkdir -p "$RUNNER_TEMP/private_keys"
echo "${{ secrets.APPLE_API_KEY }}" | base64 --decode > "$RUNNER_TEMP/private_keys/AuthKey_${{ secrets.APPLE_API_KEY_ID }}.p8"
chmod 600 "$RUNNER_TEMP/private_keys/AuthKey_${{ secrets.APPLE_API_KEY_ID }}.p8"

# macOS: build, sign (Developer ID), notarize, and publish to GitHub Releases.
- name: Build, sign & publish (macOS)
if: matrix.os == 'macos-latest'
run: npm run release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_API_KEY: ${{ runner.temp }}/private_keys/AuthKey_${{ secrets.APPLE_API_KEY_ID }}.p8
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}

# Windows / Linux: build (unsigned) and publish to GitHub Releases.
- name: Build & publish (Windows/Linux)
if: matrix.os != 'macos-latest'
run: npm run release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# Flip the draft to published only after every OS build succeeds, so a release
# never goes live (or reaches auto-update) with partial artifacts.
publish:
needs: release
runs-on: ubuntu-latest
env:
AI_GATEWAY_API_KEY: ${{ secrets.AI_GATEWAY_API_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm

- name: Install dependencies
run: npm ci

- name: Resolve previous tag
id: tags
run: |
CURRENT="${{ github.ref_name }}"
PREVIOUS=$(git tag --sort=-v:refname | grep -E '^v[0-9]' | grep -v "^${CURRENT}$" | head -1 || true)
echo "current=$CURRENT" >> "$GITHUB_OUTPUT"
echo "previous=$PREVIOUS" >> "$GITHUB_OUTPUT"

- name: Generate release notes
if: env.AI_GATEWAY_API_KEY != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ARGS=("${{ steps.tags.outputs.current }}")
if [ -n "${{ steps.tags.outputs.previous }}" ]; then
ARGS+=("${{ steps.tags.outputs.previous }}")
fi
npx tsx scripts/generate-release-notes.ts "${ARGS[@]}" > release-notes.md
gh release edit "${{ github.ref_name }}" \
--repo wassgha/rescript \
--notes-file release-notes.md

- name: Publish the draft release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release edit "${{ github.ref_name }}" \
--repo wassgha/rescript \
--draft=false --latest
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
/.next/
/out/

# production
/build
# electron compile + electron-builder packages
/electron-dist/
/dist/

# misc
.DS_Store
Expand Down
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Rescript contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
35 changes: 20 additions & 15 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,24 @@ browser, with no server, no auth, and no API calls.

```
┌────────────────────────────────────────────────────────────────────┐
│ Next.js (App Router, client-only editor) │
│ │
│ ┌───────────────┐ ┌────────────────┐ ┌───────────────────────┐ │
│ │ TranscriptPanel│ │ MediaPreview │ │ Timeline (canvas) │ │
│ │ words / cuts │ │ skip playback │ │ waveform + playhead │ │
│ └───────┬───────┘ └───────┬────────┘ └──────────┬────────────┘ │
│ └───────────── zustand store ─────────────┘ │
│ (words, cuts, playback, status) │
│ │
│ ┌──────────────────────────┐ ┌───────────────────────────────┐ │
│ │ Web Worker │ │ ffmpeg.wasm (multi-threaded) │ │
│ │ transformers.js │ │ - audio extraction (16k PCM) │ │
│ │ - Whisper (word timing) │ │ - export: trim+concat+encode │ │
│ │ - pyannote (diarization) │ └───────────────────────────────┘ │
│ └──────────────────────────┘ │
│ Electron shell (optional desktop) OR browser / GitHub Pages │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Next.js (App Router, client-only editor) │ │
│ │ │ │
│ │ ┌───────────────┐ ┌────────────────┐ ┌──────────────────┐ │ │
│ │ │ TranscriptPanel│ │ MediaPreview │ │ Timeline (canvas)│ │ │
│ │ │ words / cuts │ │ skip playback │ │ waveform+playhead│ │ │
│ │ └───────┬───────┘ └───────┬────────┘ └──────────┬───────┘ │ │
│ │ └───────────── zustand store ─────────────┘ │ │
│ │ │ │
│ │ ┌──────────────────────────┐ ┌──────────────────────────┐ │ │
│ │ │ Web Worker │ │ ffmpeg.wasm (multi-thr.) │ │ │
│ │ │ transformers.js │ │ - audio extraction │ │ │
│ │ │ - Whisper (word timing) │ │ - export trim+concat │ │ │
│ │ │ - pyannote (diarization) │ └──────────────────────────┘ │ │
│ │ └──────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ electron/main.ts — app:// static export + auto-updater │
└────────────────────────────────────────────────────────────────────┘
```

Expand Down Expand Up @@ -98,9 +101,11 @@ Everything else is derived:
6. ✅ Export: keep-range concat render to MP4 with progress, download.
7. ✅ Flexible timeline editing: word-boundary drag, Split at playhead
(scene boundaries), clip trim handles, manual cuts merged into export.
8. ✅ Electron desktop shell with signed macOS / Windows / Linux releases.

## Future work

- Native macOS SpeechAnalyzer as an optional transcription backend.
- Larger Whisper variants + language selection UI; local model import for
air-gapped first runs.
- Smarter export: stream-copy for keyframe-aligned segments, WebCodecs-based
Expand Down
45 changes: 35 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@

# Rescript

**Edit video and audio like you edit text — fully offline, in your browser.**
**Edit video and audio like you edit text — fully offline, on your device.**

**✨ Try it now: [wassgha.github.io/rescript](https://wassgha.github.io/rescript/)**
[![License: MIT](https://img.shields.io/badge/license-MIT-111?style=flat-square)](LICENSE)
[![Platforms](https://img.shields.io/badge/platforms-Web%20·%20macOS%20·%20Windows%20·%20Linux-111?style=flat-square)](#download)
[![Electron](https://img.shields.io/badge/Electron-42-111?style=flat-square&logo=electron&logoColor=9FEAF9)](https://www.electronjs.org/)
[![Stars](https://img.shields.io/github/stars/wassgha/rescript?style=flat-square&color=111)](https://github.com/wassgha/rescript/stargazers)
[![Latest release](https://img.shields.io/github/v/release/wassgha/rescript?label=latest%20release&sort=semver&style=flat-square&color=111)](https://github.com/wassgha/rescript/releases/latest)

**✨ Try it in the browser: [wassgha.github.io/rescript](https://wassgha.github.io/rescript/)**

[![Follow @wassgha on X](https://img.shields.io/badge/Follow%20@wassgha-000000?logo=x&logoColor=white)](https://x.com/wassgha)

Expand All @@ -17,6 +23,18 @@ audio file and it is transcribed locally with per-word timestamps and speaker
labels. Delete words in the transcript and the corresponding clip is cut from
the media. Export the final cut — without your file ever leaving your device.

## Download

<div align="center">

<a href="https://github.com/wassgha/rescript/releases/latest/download/Rescript-mac-arm64.dmg"><img src="assets/download/download-macos-arm64.svg" alt="Download for macOS — Apple Silicon" height="48"></a> &nbsp; <a href="https://github.com/wassgha/rescript/releases/latest/download/Rescript-mac-x64.dmg"><img src="assets/download/download-macos-x64.svg" alt="Download for macOS — Intel" height="48"></a> &nbsp; <a href="https://github.com/wassgha/rescript/releases/latest/download/Rescript-Setup.exe"><img src="assets/download/download-windows.svg" alt="Download for Windows" height="48"></a> &nbsp; <a href="https://github.com/wassgha/rescript/releases/latest/download/Rescript-linux.AppImage"><img src="assets/download/download-linux-appimage.svg" alt="Download the AppImage for Linux" height="48"></a>

</div>

See the [Releases](https://github.com/wassgha/rescript/releases) page. Desktop
builds auto-update from GitHub Releases. Prefer the browser? Use the
[web app](https://wassgha.github.io/rescript/) — same editor, no install.

- 🔒 **Private by design** — no server, no auth, no uploads; all media processing happens on-device
- 📝 **Word-level editing** — select words, press ⌫, the cut follows the text
- 📥 **Import your own transcript** — skip Whisper and edit with an SRT, VTT, or JSON caption file
Expand All @@ -30,14 +48,16 @@ the media. Export the final cut — without your file ever leaving your device.
- 🔴 **Cut edges** — drag either edge of a cut to trim independently of Whisper
timestamps; double-click to reset
- ⚡ **Live preview** — playback skips your cuts in real time
- 📦 **In-browser export** — frame-accurate MP4 (video) or M4A (audio) with ffmpeg.wasm
- 📦 **In-browser / desktop export** — frame-accurate MP4 (video) or M4A (audio) with ffmpeg.wasm
- 🎧 **Audio files** — edit podcasts, voice notes, and interviews the same way as video
- 🖥️ **Desktop app** — macOS, Windows, and Linux via Electron (signed + notarized on Mac)

## Stack

| Piece | Tech |
| --- | --- |
| App | [Next.js](https://nextjs.org) + React + TypeScript + Tailwind |
| Desktop | [Electron](https://www.electronjs.org/) + [electron-builder](https://www.electron.build/) (auto-update from GitHub Releases) |
| Transcription | [transformers.js](https://github.com/huggingface/transformers.js) running [`whisper-base_timestamped`](https://huggingface.co/onnx-community/whisper-base_timestamped) or [`whisper-small_timestamped`](https://huggingface.co/onnx-community/whisper-small_timestamped) (WebGPU with WASM fallback) in a Web Worker |
| Speaker labels | [`pyannote-segmentation-3.0`](https://huggingface.co/onnx-community/pyannote-segmentation-3.0) (ONNX) |
| Media processing | [ffmpeg.wasm](https://ffmpegwasm.netlify.app/) (multi-threaded) for audio extraction and export |
Expand All @@ -47,17 +67,20 @@ the media. Export the final cut — without your file ever leaving your device.

```bash
npm install # also copies ffmpeg/onnxruntime WASM into public/vendor
npm run dev # dev server
npm run build # production build
npm run dev # Next.js web app (http://localhost:3000)
npm run electron:dev # Electron shell + Next.js dev server
npm run build # production web build
npm run dist # unsigned desktop installers into dist/
npm run lint # eslint
```

Open [http://localhost:3000](http://localhost:3000) and drop in a video with an
audio track.
audio track. For desktop packaging, signing, and cutting releases, see
[RELEASING.md](./RELEASING.md).

> **Note on "offline":** the AI models (Whisper Base ~200 MB, or Small ~600 MB,
> plus a small speaker model) are downloaded from the Hugging Face Hub the
> *first* time you transcribe, then cached in browser storage. After that,
> *first* time you transcribe, then cached in browser / app storage. After that,
> everything — transcription, editing, export — works with the network fully
> disconnected. Your media and transcript never leave the device; the only
> third-party request the app makes is anonymous page analytics (Google
Expand All @@ -78,9 +101,11 @@ audio track.

## Browser support

A Chromium-based browser is recommended. The app requires `SharedArrayBuffer`
(served with COOP/COEP headers) and uses WebGPU for inference when available,
falling back to WASM otherwise.
A Chromium-based browser is recommended for the web app. It requires
`SharedArrayBuffer` (served with COOP/COEP headers) and uses WebGPU for
inference when available, falling back to WASM otherwise. The desktop app
bundles Chromium via Electron and sets the same isolation headers on its
`app://` protocol.

## License

Expand Down
Loading
Loading