Transcribe confidential audio. A static web page that transcribes recordings using Privatemode.ai confidential-computing inference.
dhcgn.github.io/hushscribe
Nothing to install and no account here — it runs in the browser tab you open it in.
Bring a Privatemode API key, drop in a file,
get a timestamped transcript.
You pay Privatemode about €0.014 per audio minute; a five-minute recording costs
roughly seven cents.
Your recording is encrypted before it leaves the browser tab and stays encrypted the whole way — including while the model is transcribing it. Nobody along the chain can read it: not the inference provider, not the cloud it runs on. Ordinary encryption protects data in transit and at rest; confidential computing keeps it unreadable while it is being used.
There is no backend. Bring an API key, drop a file, get a timestamped transcript.
→ ARCHITECTURE.md for how it works and why it is built this way. → AGENT.md if you are an AI agent working on this repo.
hushscribe is a front end and nothing more. Everything that makes the claim above true — the attested confidential VMs, the remote-attestation verifier, the end-to-end encryption, the models — is Privatemode, built by Edgeless Systems, a German company working on confidential computing.
This repository contributes a browser UI and the discipline needed to keep that UI trustworthy. It contributes none of the cryptography. Without their work there would be nothing here worth trusting.
Transcription is billed by Privatemode per audio minute, not per file or per request:
| Model | Rate | One hour of audio |
|---|---|---|
whisper-large-v3 |
€0.014 / min | ≈ €0.84 |
voxtral-mini-3b |
€0.004 / min | ≈ €0.24 |
Prices as of September 2026, plus VAT where applicable — see privatemode.ai/pricing for current figures. hushscribe estimates each file's cost as soon as your browser reads its length, before the transcript comes back. hushscribe itself is free and takes no cut; you pay Privatemode directly.
npm ci
npm run devThen open the page, paste a Privatemode API key, press Verify & save, and drop in an audio or video file.
| Command | What it does |
|---|---|
npm run dev |
Vite dev server on :5173 |
npm run build |
Static build into dist/ |
npm run preview |
Serve dist/ on :4173, exactly as Pages will |
npm test |
Unit tests, then the GUI suite |
npm run test:unit |
Vitest — pure logic, no browser, fast |
npm run coverage |
Vitest with coverage, and fails below the thresholds |
npm run test:e2e |
Playwright against the real production bundle |
npm run test:ui |
Playwright's UI mode, for debugging a failing test |
npm run test:smoke |
Opt-in. Transcribes test-data/ against a real enclave. Costs credit. |
No test needs an API key except test:smoke. The GUI suite injects a stand-in client at
the single seam production code exposes, so no mock code ever ships.
The coverage badge counts the four pure modules only — gate, segments, pricing,
manifest. app.js is DOM wiring, covered by Playwright against the real built bundle
rather than by Vitest, so measuring it here would report a number about the tool rather than
the code. Hence unit coverage, and this is what it means. CI enforces the thresholds on
every push, so the figure cannot quietly rot.
test-data/ holds ~83 MB of public-domain speeches (MLK, JFK, Eisenhower, a German talk)
across every supported container, plus .opus files that must be rejected. Only
test:smoke reads them. CI checks out the repo without them — a non-cone sparse
checkout paired with a blobless clone, so the bytes are never fetched on a build.
cp .env.example .env # then paste your key.env is gitignored. It prefills the key field during npm run dev and enables
test:smoke. It is substituted with '' in every build, and CI fails the pipeline if a
key-shaped string appears in dist/ — a bundle published to GitHub Pages is public.
flac mp3 mp4 mpeg mpga m4a ogg wav webm — up to 50 MB and 1 hour
per file. Anything else is rejected with the reason. Automatic re-encoding is stage 2.
Set a language to get timestamps, clickable segments, and .vtt / .srt subtitles;
leave it on auto-detect and you get plain text. That coupling is the API's, not ours —
verbose_json requires a language.
The page ships a web app manifest, so it installs like a native app and runs in its own window — no store, no packaging, the same static page either way.
Being installed changes nothing about how it works: same encryption, same attested enclave,
same localStorage. It is a shortcut and a window, not a different program.
The service worker caches nothing, deliberately. hushscribe cannot work offline — every transcription needs the API — so a cache would buy nothing, while a stale one could pin an old bundle. That bundle carries the attestation verifier and its pinned hash, so a fix must reach every client on the next load.
Everything lives in your browser's localStorage and nowhere else: the API key, saved
prompts, your last 20 transcripts. Media files are never stored — they exist only in
the tab you dropped them into, which is why history has no player. Export or delete it all
from the page.
Tick don't save new transcripts in this browser in the history panel and nothing new is written to disk at all: results stay on screen for as long as the tab is open and go no further. Forgetting the key, or clearing everything, also ends the live session rather than only deleting the stored copy.
Live at dhcgn.github.io/hushscribe.
Only tagged releases publish. A commit on main is not a version. Cut one and the
tag becomes what the site reports:
gh release create v1.2.0 --generate-notesThe footer shows that tag and links to its release notes, so the page always says which build you are looking at. Untagged builds — local, PR previews — say so instead of claiming a version they do not have.
Every pull request gets a preview at …/hushscribe/pr/<n>/, posted as a comment on
the PR and deleted when it closes. Previews skip the coverage gate (CI already ran it on
the same commit) and do not touch the released site.
One-time setup on a fresh clone or fork: Pages must serve the gh-pages branch —
production at the root, previews under pr/. The workflow cannot set this for you;
GITHUB_TOKEN is refused with Resource not accessible by integration.
gh api -X PUT repos/<owner>/<repo>/pages -f build_type=legacy -f 'source[branch]=gh-pages' -f 'source[path]=/'Or Settings → Pages → Source: Deploy from a branch → gh-pages / /. The branch itself
is created by the first deploy.
BASE_PATH defaults to /hushscribe/ for a project site — set it to / for a custom
domain, and keep it in step with base in vite.config.js. The workflow overrides it per
deploy, one level deeper for a preview.
- The enclave — the page shows the attested measurement it verified against.
- The SDK — reproducibly buildable
from
edgelesssys/privatemode-public; the build pins its Wasm SHA-256. - This page — you are reading its source. Pages publishes from this repo through a public Actions run, so the bytes you load trace back to a commit you can audit.
A trust claim nobody can trace is just a slogan.
MIT




