diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 9528ff7..426318d 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -1,8 +1,17 @@
-name: Deploy to GitHub Pages
+name: Deploy GitHub Pages redirect
+
+# The web app itself is deployed to app.getrescript.com by Vercel (see
+# vercel.json), which can serve the cross-origin isolation headers GitHub Pages
+# cannot. This workflow keeps the old wassgha.github.io/rescript URL alive as a
+# redirect so existing links, README badges, and shared URLs still land in the
+# editor.
on:
push:
branches: [main]
+ paths:
+ - "redirect/**"
+ - ".github/workflows/deploy.yml"
workflow_dispatch:
permissions:
@@ -19,22 +28,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-node@v4
- with:
- node-version: 22
- cache: npm
- uses: actions/configure-pages@v5
with:
enablement: true
- - run: npm ci
- - name: Build static export
- run: npm run build
- env:
- STATIC_EXPORT: "1"
- NEXT_PUBLIC_BASE_PATH: /${{ github.event.repository.name }}
+ - name: Assemble redirect site
+ run: |
+ mkdir -p site
+ cp redirect/index.html site/index.html
+ # Deep links (/rescript/editor) have no file on Pages, so 404.html is
+ # served for those — it runs the same path-preserving redirect.
+ cp redirect/index.html site/404.html
+ touch site/.nojekyll
- uses: actions/upload-pages-artifact@v3
with:
- path: out
+ path: site
deploy:
needs: build
diff --git a/.gitignore b/.gitignore
index 984463f..10f0dc8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,4 +43,3 @@ next-env.d.ts
# runtime assets copied from node_modules on postinstall
/public/vendor/
-/public/coi-serviceworker.js
diff --git a/README.md b/README.md
index 8c1ceb4..b685778 100644
--- a/README.md
+++ b/README.md
@@ -7,9 +7,9 @@
[](https://discord.gg/qJAhYFydat)
[](https://x.com/wassgha)
-**✨ Try it in the browser [wassgha.github.io/rescript](https://wassgha.github.io/rescript/)** or download the [Desktop App](#download)
+**✨ Try it in the browser [app.getrescript.com](https://app.getrescript.com/)** or download the [Desktop App](#download)
-[](https://wassgha.github.io/rescript/)
+[](https://app.getrescript.com/)
Rescript is an open-source, transcript-based media editor. Drop in a video or
audio file and it is transcribed locally with per-word timestamps and speaker
@@ -26,9 +26,9 @@ the media. Export the final cut — without your file ever leaving your device.
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.
+[web app](https://app.getrescript.com/) — same editor, no install.
-- 🔒 **Private by design** — no server, no auth, no uploads; all media processing happens on-device
+- 🔒 **Private by design** — 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
- 📤 **Export hub** — video (MP4/WebM, 720p–4K), audio (M4A/MP3/WAV), transcript (TXT/MD), or subtitles (SRT/VTT/JSON)
@@ -78,12 +78,13 @@ audio track. For desktop packaging, signing, and cutting releases, see
> *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
-> Analytics), which fails silently when offline.
+> third-party requests the app makes are anonymous page analytics (Google
+> Analytics and Vercel Web Analytics — page views and app opens, nothing about
+> your media), which fail silently when offline.
## How it works
-[](https://wassgha.github.io/rescript/)
+[](https://app.getrescript.com/)
1. **Extract** — ffmpeg.wasm decodes the audio track to mono 16 kHz PCM.
2. **Transcribe** — Whisper runs in a Web Worker with `return_timestamps: "word"`,
diff --git a/ROADMAP.md b/ROADMAP.md
index 0b9845f..895dedd 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -6,7 +6,7 @@ User-facing progress for Rescript. Items are marked done when they shipped to
## Done
### Core editor
-- [x] Offline transcript-based editor in the browser (no server, no auth, no uploads)
+- [x] Offline transcript-based editor in the browser (no auth, no uploads)
- [x] Upload → local Whisper transcription with per-word timestamps
- [x] Speaker diarization (pyannote) with speaker-grouped transcript
- [x] Cut / restore by selecting words (⌫, floating toolbar); undo / redo
diff --git a/app/layout.tsx b/app/layout.tsx
index 4e4e866..4bb0655 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -3,7 +3,6 @@ import { Geist, Geist_Mono } from "next/font/google";
import Script from "next/script";
import { GoogleAnalytics } from "@next/third-parties/google";
import { Analytics } from "@vercel/analytics/next";
-import { isElectron } from "@/lib/platform";
import "./globals.css";
const geistSans = Geist({
@@ -16,13 +15,12 @@ const geistMono = Geist_Mono({
subsets: ["latin"],
});
-const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
-
-// Vercel Web Analytics is collected by the deployment at getrescript.com.
-// Neither GitHub Pages nor the Electron app:// shell can serve the default
-// /_vercel/insights paths, so point the SDK at absolute URLs there. Left unset
-// in development so the SDK loads its debug script and logs to the console
-// instead of reporting local traffic.
+// Vercel Web Analytics for both the web app and the desktop shell is collected
+// by the deployment at getrescript.com, so the two show up in one dashboard.
+// The Electron app:// shell cannot serve the default /_vercel/insights paths at
+// all, so the SDK is pointed at absolute URLs through that project's CORS
+// proxy. Left unset in development so the SDK loads its debug script and logs
+// to the console instead of reporting local traffic.
const analyticsHost =
process.env.NEXT_PUBLIC_VERCEL_ANALYTICS_HOST ?? "https://www.getrescript.com";
const analyticsProps =
@@ -38,18 +36,18 @@ const description =
"A fully offline, open-source transcript-based video editor. Transcribe with Whisper, cut by deleting words, export with ffmpeg — on your device.";
export const metadata: Metadata = {
- metadataBase: new URL("https://wassgha.github.io"),
+ metadataBase: new URL("https://app.getrescript.com"),
title,
description,
openGraph: {
type: "website",
siteName: "Rescript",
- url: `${basePath}/`,
+ url: "/",
title,
description,
images: [
{
- url: `${basePath}/og.png`,
+ url: "/og.png",
width: 1200,
height: 630,
alt: "Rescript — a transcript-based video editor running in the browser",
@@ -60,7 +58,7 @@ export const metadata: Metadata = {
card: "summary_large_image",
title,
description,
- images: [`${basePath}/og.png`],
+ images: ["/og.png"],
},
};
@@ -84,17 +82,6 @@ export default function RootLayout({
strategy="beforeInteractive"
dangerouslySetInnerHTML={{ __html: appearanceBootScript }}
/>
- {/* Provides COOP/COEP via a service worker on static hosts (GitHub
- Pages) that can't send headers; no-op when the server already
- sends them. Required for SharedArrayBuffer / multi-threading.
- Skipped in the Electron shell — the app:// protocol sets the
- headers directly. */}
- {!isElectron && (
-
- )}
{children}
diff --git a/components/UploadScreen.tsx b/components/UploadScreen.tsx
index e281a50..fdc02f6 100644
--- a/components/UploadScreen.tsx
+++ b/components/UploadScreen.tsx
@@ -165,9 +165,8 @@ export default function UploadScreen({
const [dragging, setDragging] = useState(false);
const [projects, setProjects] = useState([]);
const [busyId, setBusyId] = useState(null);
- // The pipeline needs SharedArrayBuffer, which on static hosts only appears
- // after the COI service worker reloads the page. Accepting a file before then
- // would fail immediately and lose the file to that reload.
+ // The pipeline needs SharedArrayBuffer, so don't accept a file until the page
+ // is confirmed cross-origin isolated — transcription would fail immediately.
const isolation = useCrossOriginIsolated();
const ready = isolation === "ready";
const source = useEditorStore((s) => s.source);
diff --git a/eslint.config.mjs b/eslint.config.mjs
index d79a2fd..49e046f 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -18,7 +18,6 @@ const eslintConfig = defineConfig([
"dist/**",
// Runtime assets copied from node_modules on postinstall.
"public/vendor/**",
- "public/coi-serviceworker.js",
]),
]);
diff --git a/hooks/useCrossOriginIsolated.ts b/hooks/useCrossOriginIsolated.ts
index 3879fdc..207d1df 100644
--- a/hooks/useCrossOriginIsolated.ts
+++ b/hooks/useCrossOriginIsolated.ts
@@ -2,10 +2,7 @@
import { useSyncExternalStore } from "react";
-export type IsolationState = "checking" | "preparing" | "ready" | "unavailable";
-
-/** How long to wait for the COI service worker to take over before giving up. */
-const PREPARE_TIMEOUT_MS = 10_000;
+export type IsolationState = "checking" | "ready" | "unavailable";
function isIsolated() {
return self.crossOriginIsolated && typeof SharedArrayBuffer !== "undefined";
@@ -23,37 +20,14 @@ function emit(next: IsolationState) {
for (const listener of listeners) listener();
}
-function watch() {
- if (isIsolated()) return emit("ready");
- // A service worker already controlling an un-isolated page means the worker
- // cannot fix this (insecure context, stripped headers, or a browser without
- // SharedArrayBuffer) — waiting for a reload that never comes is worse than
- // saying so.
- const sw = navigator.serviceWorker;
- if (!window.isSecureContext || !sw || sw.controller) return emit("unavailable");
-
- emit("preparing");
- // coi-serviceworker reloads as soon as it takes control, so normally the page
- // goes away before this resolves. Poll anyway — the reload can be blocked (or
- // skipped by a future version) and we still want to notice isolation.
- let waited = 0;
- const timer = setInterval(() => {
- waited += 250;
- if (isIsolated()) {
- clearInterval(timer);
- emit("ready");
- } else if (waited >= PREPARE_TIMEOUT_MS) {
- clearInterval(timer);
- emit("unavailable");
- }
- }, 250);
-}
-
function subscribe(listener: () => void) {
listeners.add(listener);
if (!watching) {
watching = true;
- watch();
+ // Isolation is decided by the response headers of the document itself, so it
+ // is already settled by the time any component mounts — there is nothing to
+ // poll for or wait on.
+ emit(isIsolated() ? "ready" : "unavailable");
}
return () => listeners.delete(listener);
}
@@ -62,11 +36,11 @@ function subscribe(listener: () => void) {
* Reports whether the page can use SharedArrayBuffer, which ffmpeg.wasm and
* onnxruntime both require for multi-threading.
*
- * On static hosts isolation comes from public/coi-serviceworker.js, which
- * registers itself and then reloads the page — so on a first visit the app is
- * briefly interactive *without* isolation. Anything started in that window dies
- * with "SharedArrayBuffer is not defined" (and loses its work to the reload),
- * so the UI waits for "ready" before touching the pipeline.
+ * Isolation comes from real COOP/COEP headers on every target: vercel.json for
+ * the web app, the app:// protocol handler for Electron, and next.config.ts
+ * headers() for `next dev`. "unavailable" therefore means the browser itself
+ * can't do it (insecure context, or no SharedArrayBuffer), not that we're still
+ * setting up — so the UI can say so immediately instead of showing a spinner.
*/
export function useCrossOriginIsolated(): IsolationState {
return useSyncExternalStore(
diff --git a/lib/ffmpeg.ts b/lib/ffmpeg.ts
index 9cc96c5..b17e523 100644
--- a/lib/ffmpeg.ts
+++ b/lib/ffmpeg.ts
@@ -3,8 +3,7 @@
import type { FFmpeg } from "@ffmpeg/ffmpeg";
import type { TimeRange } from "./types";
-const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
-const CORE_BASE = `${BASE_PATH}/vendor/ffmpeg`;
+const CORE_BASE = "/vendor/ffmpeg";
const INPUT_NAME = "input_video";
let ffmpegPromise: Promise | null = null;
@@ -19,8 +18,8 @@ export async function getFFmpeg(): Promise {
import("@ffmpeg/util"),
]);
// Multi-threaded ffmpeg.wasm needs SharedArrayBuffer, i.e. a
- // cross-origin-isolated page (real COOP/COEP headers, or the COI service
- // worker after its reload). Without it the core throws a bare
+ // cross-origin-isolated page (COOP/COEP from vercel.json on the web, from
+ // the app:// handler in Electron). Without it the core throws a bare
// "SharedArrayBuffer is not defined" from deep inside the worker.
if (!self.crossOriginIsolated || typeof SharedArrayBuffer === "undefined") {
throw new Error(
@@ -34,8 +33,7 @@ export async function getFFmpeg(): Promise {
workerURL: await toBlobURL(`${CORE_BASE}/ffmpeg-core.worker.js`, "text/javascript"),
// Served same-origin (copied on postinstall): the bundled class worker
// contains a dynamic import() that Next's bundler cannot handle.
- classWorkerURL: new URL(`${BASE_PATH}/vendor/ffmpeg-class/worker.js`, location.href)
- .href,
+ classWorkerURL: new URL("/vendor/ffmpeg-class/worker.js", location.href).href,
});
return ffmpeg;
})();
diff --git a/next.config.ts b/next.config.ts
index 53bb86e..5defa72 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,11 +1,11 @@
import type { NextConfig } from "next";
-// Static export for GitHub Pages: set STATIC_EXPORT=1 and (for project pages
-// served under //) NEXT_PUBLIC_BASE_PATH=/. Static hosts cannot
-// send custom headers, so cross-origin isolation is provided at runtime by
-// public/coi-serviceworker.js instead of the headers() below.
+// STATIC_EXPORT=1 emits the static bundle shipped to both targets: the web app
+// at app.getrescript.com (served by Vercel, which sends the cross-origin
+// isolation headers from vercel.json) and the Electron shell (which sets them
+// itself in electron/main.ts). Both serve from the root, so there is no
+// basePath. The headers() below only covers `next dev`, where neither applies.
const isExport = process.env.STATIC_EXPORT === "1";
-const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
const nextConfig: NextConfig = {
reactStrictMode: true,
@@ -14,7 +14,6 @@ const nextConfig: NextConfig = {
...(isExport
? {
output: "export" as const,
- basePath,
images: { unoptimized: true },
}
: {
diff --git a/package-lock.json b/package-lock.json
index 1561f13..9aa85c4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,7 +17,6 @@
"@huggingface/transformers": "^4.2.0",
"@next/third-parties": "^16.2.12",
"@vercel/analytics": "^2.0.1",
- "coi-serviceworker": "^0.1.7",
"lucide-react": "^1.27.0",
"next": "16.2.12",
"parakeet.js": "^1.4.4",
@@ -4596,12 +4595,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/coi-serviceworker": {
- "version": "0.1.7",
- "resolved": "https://registry.npmjs.org/coi-serviceworker/-/coi-serviceworker-0.1.7.tgz",
- "integrity": "sha512-bjSUqEngCPOkErY2vbyWsaIGCNRODYzlNycaREVw5s12/C8SM+RnRUUeX6pZbTtov6C52ZLY/+tvHK+BDxuUuA==",
- "license": "MIT"
- },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
diff --git a/package.json b/package.json
index ab858ef..1aaac7d 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"build:electron": "node scripts/build-electron.mjs",
"typecheck:electron": "tsc -p electron/tsconfig.json --noEmit",
"electron:dev": "npm run build:electron && concurrently -k -n next,electron -c cyan,magenta \"next dev\" \"node scripts/electron-dev.mjs\"",
+ "export:web": "cross-env STATIC_EXPORT=1 next build",
"export:desktop": "cross-env STATIC_EXPORT=1 NEXT_PUBLIC_ELECTRON=1 next build",
"dist": "npm run export:desktop && npm run build:electron && electron-builder --config.npmRebuild=false",
"release": "npm run export:desktop && npm run build:electron && electron-builder --publish always --config.npmRebuild=false",
@@ -39,7 +40,6 @@
"@huggingface/transformers": "^4.2.0",
"@next/third-parties": "^16.2.12",
"@vercel/analytics": "^2.0.1",
- "coi-serviceworker": "^0.1.7",
"lucide-react": "^1.27.0",
"next": "16.2.12",
"parakeet.js": "^1.4.4",
diff --git a/public/.nojekyll b/public/.nojekyll
deleted file mode 100644
index 8b13789..0000000
--- a/public/.nojekyll
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/redirect/index.html b/redirect/index.html
new file mode 100644
index 0000000..f14cda1
--- /dev/null
+++ b/redirect/index.html
@@ -0,0 +1,64 @@
+
+
+
+
+
+ Rescript has moved to app.getrescript.com
+
+
+
+
+
+
+
+
+
+