@hushvert/engine
v0.1.0
Published
Client-side file conversion engine: images, archives, PDF page ops, audio, small video and DOCX preview as WebAssembly in the browser. Your files never leave your device.
Downloads
128
Readme
@hushvert/engine
File conversion that never uploads your files.
Every conversion in this package runs inside the browser: WebAssembly codecs in Web Workers, on the user's own CPU. There is no server, no upload endpoint, no queue and no retention policy to trust, because the bytes never cross the network. Open the network tab while a conversion runs and watch nothing leave.
This is the open-source client engine behind hushvert, the private converter.
What it converts
| Module | Pairs | Powered by |
| -------------- | ---------------------------------------------------- | --------------------------- |
| images | png / jpg / webp / avif interchange, heic to all four | @jsquash/*, heic-to |
| archives | tar / tar.gz to zip, plus listing and per-entry extraction | libarchive.js, @zip.js/zip.js |
| pdf | merge n PDFs into one | @cantoo/pdf-lib |
| audio-video | mp3 / wav / m4a / mp4 audio, mp4 to webm (small video) | ffmpeg.wasm (singlethread) |
| docx-preview | docx to clean semantic HTML | mammoth |
Usage
import { configureEngine, convertFile, listArchive } from '@hushvert/engine'
// Once at startup: where the host serves the wasm worker assets (see below).
configureEngine({
libarchiveWorkerUrl: '/vendor/libarchive/worker-bundle.js',
ffmpeg: {
coreUrl: '/vendor/ffmpeg/ffmpeg-core.js',
wasmUrl: '/vendor/ffmpeg/ffmpeg-core.wasm',
classWorkerUrl: '/vendor/ffmpeg/worker.js',
},
})
const blob = await convertFile(
file,
{ from: 'mp3', to: 'wav', module: 'audio-video' },
(pct) => console.log(`${pct}%`),
)The pair argument is a plain object: from, to and which module runs
it (plus op: 'merge' for multi-file merges via convertFiles). Keep your
own format matrix as the routing authority and pass its rows through.
Serving the wasm assets
Two modules need assets served by your app, same-origin (which is also what keeps the privacy claim verifiable). Copy out of node_modules at build time:
libarchive.js/dist/worker-bundle.jsandlibarchive.js/dist/libarchive.wasminto one directory; pass the bundle URL aslibarchiveWorkerUrl.@ffmpeg/core/dist/esm/ffmpeg-core.js+ffmpeg-core.wasm, and@ffmpeg/ffmpeg/dist/esm/worker.js+const.js+errors.jsinto one directory; pass the three URLs inffmpeg.
Everything else (image codecs, zip.js, pdf-lib, mammoth) is bundled by your
bundler; the engine creates its workers with the standard
new Worker(new URL(...), { type: 'module' }) pattern that webpack, Vite and
friends all understand. The package ships TypeScript source and expects to be
consumed through a bundler.
Why singlethread ffmpeg
The multithreaded ffmpeg core needs SharedArrayBuffer, which requires COOP/COEP headers on every embedding page; COEP in turn breaks common third-party embeds (CAPTCHAs, ad iframes) on at least some browsers. The singlethread core runs everywhere with zero header requirements and proved fast enough for audio and small video. Hosts who control their headers can swap in the multithreaded core URLs without engine changes.
Limits worth knowing
- Audio inputs were tested to 1GB on desktop Chrome; recommend capping near 200MB for low-memory devices.
- Video encoding is singlethread wasm: budget roughly real time. Keep inputs small (hushvert caps mp4 to webm at 50MB) and route bigger jobs elsewhere.
- The docx preview is semantic by design (mammoth): structure survives, page-perfect layout does not.
