Self-hosted transcription for audio and video. Runs faster-whisper in a
container on your own hardware — the file never leaves the machine, and it works
with the network cable pulled.
Transcription services want your recordings. Interviews, meetings, medical notes, legal calls — the material people most want transcribed is exactly the material they should be most careful about uploading somewhere. An optional hosted engine exists for when speed matters more than privacy; it is off unless you turn it on, and the interface says so before you upload anything.
Tested rather than asserted. With the models cached, the whole stack was
recreated on a Docker network marked internal: true — no route off the machine
— and a file put through the complete path: upload, ffmpeg, whisper, all four
output formats.
app -> https://example.com BLOCKED
whisper -> https://huggingface.co BLOCKED (gaierror)
whisper container health healthy — models loaded from cache
upload -> transcript, full path COMPLETED in 4.64 s
It completed because it never needed the network. Note the third line: the whisper service is healthy while cut off, so cached weights load without the library reaching for the Hub — the failure mode most likely to make an "offline" tool quietly not be one. Transcript contents are never written to a log, at any level.
The one exception is the first run, which fetches the weights. It is logged plainly and happens once. → How to reproduce this, and how to pre-load safely
git clone <this repository> transcriber && cd transcriber
cp .env.example .env
docker compose build
docker compose run --rm --no-deps app php artisan key:generate
docker compose run --rm --no-deps app php artisan migrate --force
# Optional on loopback, mandatory anywhere else:
docker compose run --rm --no-deps app php artisan transcriber:hash-password
docker compose up -dOpen http://localhost:8000. Choose a file, press Transcribe, download TXT, SRT, VTT or JSON. Per-job options live under Advanced; the main path stays one click.
The first start downloads 1.7 GB of weights and looks idle while it does.
That is the download, not a hang — watch docker compose logs -f whisper. If port
8000 is taken, set APP_PORT in .env.
| Form label | Model | On disk | 11 s clip | 5 min clip | Take it when |
|---|---|---|---|---|---|
| Fast (default) | base |
142 MB | 1.0 s | 59 s | Clear speech you can spot-check. ~5× real time. |
| More accurate | large-v3-turbo |
1.6 GB | 11.4 s | 185 s | Accents, names, several speakers, noise. ~1.6× real time. |
Measured on a 2026 laptop CPU, no GPU, int8, beam_size=5, through the real
engine — not copied from a datasheet. The accurate model is 11× slower on the
short clip and 3× on the long one: its fixed per-run cost dominates a short
recording and disappears into a long one.
The timings are honest; do not read them as a verdict on accuracy. They were
taken on samples/jfk.wav, which is close to the best case a small model can be
given — studio recording, one speaker, careful diction, no background noise, and
a canonical clip the models have effectively seen. On audio like that the two
models produce nearly identical text, and Fast is obviously the right choice.
That is not where the difference lives. Reach for More accurate on accented speech, background noise, phone- or video-call quality, overlapping speakers, proper nouns, and specialist vocabulary. On any of those the gap is real and routinely worth the wall clock. A clean-audio benchmark cannot show you that, and this one does not claim to.
Both stay resident once used. The set is closed on purpose — an arbitrary model name would be fetched from the Hub the first time it was used, which is an outbound request in the middle of a job. → Models
TranscriptionEngine is the seam. LocalWhisperEngine talks to the whisper
container over HTTP rather than shelling out, which is why GroqEngine is a
sibling rather than a special case. Transcript is the other boundary: engines
produce one, formatters consume one, and adding a format never opens an engine
file.
Engines declare what they support, so silently ignoring an option is not an available behaviour — the form offers only what the engine can do, a missing critical option refuses the job, a missing non-critical one is dropped and reported. The line is about visibility, not importance: fail when an unapplied option corrupts the result invisibly, record and continue when the degradation is visible in the output itself.
CI never downloads a model — the suite runs against a FakeEngine implementing
the same contract. → Architecture
- Localhost only. One password, no TLS. Forward the port without a TLS-terminating proxy and your recordings and your password travel in clear text. → Deployment security, which does not soften this.
- No YouTube or third-party site downloading. It violates their Terms of Service and makes a public repository a liability. Direct media URLs are fine; site scrapers are not. A scope decision, not something unfinished.
- Single user. No accounts, no roles, no audit trail.
- No speaker diarization, no real-time transcription. Out of scope for v1.
- Progress is measured, and therefore uneven. The bar is the fraction of the recording decoded so far, not a timer. It jumps where silence was skipped, slows on hard audio, and stalls short of the end when a recording finishes quietly. It is not smoothed. The hosted engine returns its transcript in one piece and reports no progress at all — the page then shows the state alone rather than an empty bar.
- CPU is slow, honestly. A one-hour recording is ~12 minutes on Fast and most of an hour on More accurate.
| Models | Sizes, measured speeds, the full family, adding one, the cache volume. |
| Access | The password gate, hashing, sessions, and the $ trap that eats bcrypt hashes. |
| Configuration | Every environment variable and what changing it costs. |
| Operations | Ports, hangs, retention, the queue, upgrades, backups. |
| Deployment security | Read before exposing this. |
| Architecture | The seam, the capability rule, testing the real engine. |
| Changelog | What is in each release, and the known limitations of this one. |
app/Transcription/
Contracts/TranscriptionEngine.php the seam — engines implement this
Transcript.php, Segment.php segments + text, format-agnostic
Engines/{LocalWhisper,Groq,Fake}Engine.php
Formatters/{Txt,Srt,Vtt,Json}Formatter.php
Jobs/TranscribeUpload.php queued, retryable
MediaNormaliser.php the only place anything is shelled out to
docker/whisper/ Python service wrapping faster-whisper
docs/ full operational documentation
MIT.