Add subtitles to any video and burn them in. All in the browser. No upload, no account.
Tscaps is a client-side video editor for captions. Drop a video in, transcribe it with in-browser Whisper, style the captions through CSS, and export the result frame by frame to a new video — all without a backend.
Captions are HTML elements styled with CSS. In the editor, that DOM is layered live over the video during playback. On export, the engine rasterizes the same DOM at each frame's timestamp into a bitmap and composites it with the video frame. The browser renders the captions in both cases.
example_1.mp4 |
example_2.mp4 |
example_3.mp4 |
| Path | What it is |
|---|---|
packages/engine |
The framework-agnostic TypeScript engine that does the rendering. Published to npm as @tscaps/engine. |
apps/studio |
The web app that wraps the engine in a UI — drop a video, edit captions, export. |
templates |
The visual-style gallery the editor consumes. Each template is a folder of JSON + CSS. |
A hosted version of tscaps runs at tscaps.io with two surfaces sharing the same editor:
- Local — the same in-browser flow this repository ships. Free, private, no signup. Transcription via in-browser Whisper, hardware-bound (speed and accuracy depend on the device; mobile is rough).
- Cloud — server-side transcription for higher accuracy and speed, AI-driven scene styling, multi-device project sync. Free tier with a watermark; paid tiers lift caps and remove the watermark.
The cloud surface's server is not open-source. What ships in this repository is the open-source equivalent of the local surface — the same editor, the same engine, the same templates, with no server in the loop. Self-host it, fork it, embed the engine in your own product.
The fastest path:
docker run -p 8080:80 ghcr.io/francozanardi/tscaps-web:latestOpen http://localhost:8080. The image is a static nginx serving the production bundle.
If you want to customise the build (templates, branding, environment), build the image locally. Build context is the workspace root:
docker build -f apps/studio/Dockerfile -t tscaps-web .
docker run -p 8080:80 tscaps-webpnpm install
pnpm --filter ./apps/studio devOpen the URL the dev server prints. Drop a video and the editor opens with the transcribe flow ready.
To produce a static bundle:
pnpm --filter ./apps/studio buildOutput lands in apps/studio/dist/.
The engine ships separately so you can embed it in your own product without bringing the editor UI along.
npm install @tscaps/engineimport { RenderPipelineBuilder } from '@tscaps/engine';
const inputVideo: Blob = /* from a file input, fetch, etc. */;
const pipeline = new RenderPipelineBuilder()
.withInputVideo(inputVideo)
.build();
const { blob } = await pipeline.run();
// `blob` is a Blob containing the captioned mp4The full pipeline API, every styling knob, every transcriber, every splitter, the document model, and the tag system live in packages/engine/README.md, with worked examples and GIFs of each result.
A template is a self-contained visual style for burned-in subtitles — a folder containing a template.json (metadata, controls, alignment), a style.css (the actual visual rules), and any assets the CSS references. The editor and the engine both pick up every folder under templates/; adding a new look is "drop the folder in."
The author guide — folder layout, the universal CSS variable contract that lets editor controls drive the visual, animation patterns under paused playback, SVG filters, the live-vs-export differences and traps, and an author's checklist — is in templates/README.md.
If you've never written one and want to learn by reading: the existing templates under templates/ are the canonical examples, ordered roughly by complexity.
Templates are the easiest way to leave a fingerprint on this project. The CSS contract is documented end-to-end, the existing folders are working references, and a good template can ship in a single PR with zero build-system changes. If you have a caption look you've always wanted and can write CSS, you can ship it here — and every user of the editor will see it in the gallery.
Open a PR with a new folder under templates/ and the editor picks it up automatically.
Every transcriber produces a Document. Templates style it. The hierarchy is:
Document
└── Section[] contiguous run, processed by one splitter + tagger chain
└── Segment[] one screen-sized caption block, carries a time range
└── Line[] one visible line of text within a segment
└── Word[] a word with text, time range, and tag set
The render layer exposes that tree to CSS through three surfaces — a flat set of CSS classes per element, a flat set of CSS custom properties per element, and a tag system that adds more classes via taggers. Every styling decision a template makes targets one of those three surfaces.
The full description lives in packages/engine/README.md.
Pre-1.0. The public engine API is stabilising but may shift between minor versions until 1.0. The web app is a moving target — features land regularly. Pin to an exact version (or commit) in production and review the changelog before upgrading.
Issues, PRs, and template contributions are welcome. The repo is a pnpm monorepo; see the per-package READMEs for run/build/test commands. New templates are especially welcome — see Contribute a template above.