A fully local and private Speech-To-Text app, offering multiple model backends, diarization & calendar mode - Available for Windows, macOS & Linux
720
stars
1,611
commits
TypeScript
primary language
Sep 5, 2026
updated
|
Turn speech into text - on your own computer.
TranscriptionSuite is a free, open-source transcription app. Record a lecture, dictate a document, or import an audio file, and get an accurate transcript with speaker labels in minutes. Everything runs locally: your audio never leaves your machine. Under the hood: an Electron dashboard + Python backend with multi-backend speech-to-text (Whisper, NVIDIA NeMo, VibeVoice-ASR, SenseVoice, whisper.cpp, MLX), accelerated by NVIDIA CUDA, Apple Metal, or AMD/Intel Vulkan - or plain CPU. Dockerized for fast setup. |
OS Support: Hardware Acceleration: |
TranscriptionSuite is two parts working as one app:
Day-to-day use is entirely point-and-click; a terminal is only needed for a few one-time setup steps on some platforms. Want to get going right away? Jump to Installation and pick your platform. Wondering whether your computer can run it? Check the Compatibility Matrix.
.txt), subtitles (.srt/.ass), or both, straight to a folder of your choice (no Notebook entry is created).📌Half an hour of audio transcribed in under a minute with Whisper (RTX 3060)!
*What downloads on first use: the STT model weights themselves, plus the PyAnnote diarization and wav2vec2 alignment models if you use them. Everything is cached locally and nothing further is fetched after that.
Not every model runs on every machine. Not sure what any of this means? You can skip this section - the app detects your hardware, pre-selects working defaults, and greys out incompatible choices with the reason shown.
A runtime is the engine mode the app uses to run the models, and it maps to your hardware: NVIDIA GPU = CUDA, Apple Silicon = Metal, AMD/Intel GPU = Vulkan, any machine = CPU. The tables below show which runtime fits your computer, and what each model family can do on it.
Where each runtime runs (runtime names as shown in the app):
| Runtime | Linux NVIDIA | Linux AMD/Intel | Windows NVIDIA | Windows AMD/Intel | macOS Apple Silicon | macOS Intel |
|---|---|---|---|---|---|---|
| CUDA (Docker) | Yes | No | Yes | No | No | No |
| CPU Only (Docker) | Yes | Yes | Yes | Yes | Yes¹ | Yes |
| Vulkan Linux (Docker + sidecar helper container) | No² | Yes | No | No | No | No |
| Vulkan Windows (Docker + native whisper-server) | No | No | No² | Yes | No | No |
| Metal (native, no Docker) | No | No | No | No | Yes | No |
¹ Possible, but Apple Silicon Macs should use Metal instead - it's native and much faster. ² When an NVIDIA GPU is detected, the app greys out the Vulkan runtimes ("NVIDIA detected" badge) and steers you to CUDA.
Models × runtimes × features:
| Model family | Runtime(s) | Live Mode | Translation | Diarization |
|---|---|---|---|---|
| Faster-Whisper (WhisperX) | CUDA, CPU | Yes | To English (not turbo/.en variants) | PyAnnote (token) |
| Parakeet v3 (NeMo) | CUDA | No | No | PyAnnote (token) |
| Canary v2 (NeMo) | CUDA | No | Yes - bidirectional, 25 languages | PyAnnote (token) |
| SenseVoice (FunASR) | CUDA, CPU (slow) | No | No | CAM++ built-in (no token) or PyAnnote |
| VibeVoice-ASR | CUDA, CPU (very slow) | No | No | Built-in |
| Whisper.cpp (GGML) | Vulkan Linux/Windows | Yes | To English (not turbo/.en variants) | No |
| MLX Whisper | Metal | Via faster-whisper (CPU) | To English (not turbo/.en variants) | Sortformer (≤ 4 speakers) or PyAnnote |
| MLX Parakeet v3 | Metal | Via faster-whisper (CPU) | No | Sortformer or PyAnnote |
| MLX Canary v2 | Metal | Via faster-whisper (CPU) | No (MLX port) | Sortformer or PyAnnote |
| MLX VibeVoice-ASR | Metal | Via faster-whisper (CPU) | No | Built-in |
Notes:
- Live Mode always runs on faster-whisper or whisper.cpp models. On Apple Silicon the Metal server bundles faster-whisper for exactly this - Live Mode works, but decodes on the CPU rather than the GPU.
- PyAnnote diarization needs a free HuggingFace token and accepting the model's terms; the app asks for the token during first-start setup. CAM++ (SenseVoice), Sortformer (Apple Silicon), and VibeVoice's built-in diarization need no token.
- Where diarization runs: file imports (Session tab > Import, Notebook tab > Import) and normal recordings on the Session tab, when the Speaker Diarization switch is on. Live Mode never diarizes - speaker attribution needs the whole recording, which a streaming transcriber does not have.
- Sortformer handles up to 4 speakers; pick PyAnnote on Apple Silicon for larger groups.
- NeMo models (Parakeet/Canary) are unavailable on the CPU runtime - the app substitutes a faster-whisper model instead.
- NeMo models can't output the Greek final sigma (ς), so Greek word endings may be truncated; the app shows a warning when this applies.
- VibeVoice-ASR is a 9-billion-parameter model (~16 GB download) - it wants a GPU with plenty of VRAM and is very slow on CPU.
- Model weights that are missing get downloaded automatically when the server starts - there is no separate download step.
- Older NVIDIA cards (GTX 10-series and earlier) use the same CUDA runtime with the CUDA Legacy image variant - see §2.6.
| Session Tab | Notebook Tab |
|---|---|
![]() | ![]() |
| Audio Note View | Server Tab |
|---|---|
![]() | ![]() |
The Releases page contains the Dashboard only - the server is downloaded from inside the app. Pick the section for your platform:
| Your computer | Section | How the server runs |
|---|---|---|
| Apple Silicon Mac (M1+) | § 2.1 | Natively on the Mac - no Docker |
| Intel Mac (pre-M1) | § 2.2 | Docker / Podman (CPU only) |
| Windows | § 2.3 | Docker / Podman |
| Linux | § 2.4 | Docker / Podman |
For Macs with an Apple chip (M1 and later). This is the easiest setup: the server runs natively on your Mac's GPU - no Docker, no Python, no separate server install.
Apple Silicon transcription runs on Apple's Metal + MLX GPU stack, bundled right inside the app. You download one file, drag it to Applications, and click Start Metal Server.
Naming note: wherever the app says "Metal server", what actually runs is MLX - Apple's machine-learning framework, which uses Metal as its GPU layer. MLX only exists on Apple Silicon, which is why Intel Macs (§2.2) can't use this path.
Prerequisite - FFmpeg: the server uses FFmpeg to decode compressed audio (M4A, MP3, FLAC, OGG - everything except WAV), and macOS doesn't ship it. Install it once with Homebrew:
brew install ffmpeg
Without it the server still starts, but importing anything other than a WAV file fails with a decode error. If you install FFmpeg while the app is running, restart the Metal server afterwards.
Install steps:
TranscriptionSuite-<version>-arm64-mac-metal.dmg (~3-5 GB) - the bundled build with the dashboard and the Python/MLX server pre-installed. (Careful: there is also a thin arm64-mac.dmg without the server - see the tip below.)xattr -dr com.apple.quarantine /Applications/TranscriptionSuite.app
Want to run the server somewhere else instead? If you'd rather use this Mac only as a remote control for a server on another machine (e.g. a PC with an NVIDIA GPU), or run the server locally in Docker, download the thin
arm64-mac.dmg(~200 MB, dashboard only) instead. Then set up a Docker server via §2.5-§2.6, or connect to another machine via §3 Remote Connection.
For older Intel-based Macs (pre-M1). The server runs in Docker on the CPU - it works, but expect it to be slow. Intel Macs get no GPU acceleration here, because Apple's MLX is Apple-Silicon-only.
x64-mac.dmg buildWindows runs the server in Docker (or Podman). With an NVIDIA GPU you get full acceleration; with an AMD or Intel GPU use the Vulkan path (§2.7); otherwise the server runs on CPU.
wsl --list --verbose in a terminal - the version column should read 2.Linux runs the server in Docker (or Podman). With an NVIDIA GPU you get full acceleration; with an AMD or Intel GPU use the Vulkan path (§2.7); otherwise the server runs on CPU.
Pick Docker or Podman - the app auto-detects whichever is installed (Docker is checked first).
Docker:
sudo pacman -S --needed dockerdocker group so the app can use Docker without sudo:
sudo usermod -aG docker $USER
Then log out and back in (or reboot) for it to take effect.sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
Skip this for CPU mode. (The app automatically prefers CDI when a CDI spec exists; if a later driver update breaks GPU access, see GPU not working after a system update.)Podman (4.7+ required for podman compose):
sudo pacman -S --needed podmansystemctl --user enable --now podman.socket
Without it, compose commands fail with "Cannot connect to the Docker daemon" even though podman itself works.sudo nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml
Skip this for CPU mode.Then continue with the shared steps: §2.5 Download the Dashboard app and §2.6 Setting Up the Server.
Download and install the Dashboard for your platform from the Releases page. This is just the frontend - no models or packages are downloaded yet - but it must be installed before you set up the server in §2.6.
Which file to download:
| Platform | File |
|---|---|
| Linux (x64) | TranscriptionSuite-<version>.AppImage |
| Windows (x64) | TranscriptionSuite Setup <version>.exe |
| Mac - Apple Silicon, bundled server | TranscriptionSuite-<version>-arm64-mac-metal.dmg (§2.1) |
| Mac - Apple Silicon, thin (dashboard only) | TranscriptionSuite-<version>-arm64-mac.dmg |
| Mac - Intel | TranscriptionSuite-<version>-x64-mac.dmg (§2.2) |
Only x64 builds exist for Linux and Windows - there are no ARM builds for those platforms. Each release file is signed with my GPG key (the matching
.ascfile); verifying is optional - see §2.5.2.
macOS only: after dragging the app into Applications, clear the "unverified app" quarantine once in Terminal:
xattr -dr com.apple.quarantine /Applications/TranscriptionSuite.app
AppImages need FUSE 2 (libfuse.so.2), which isn't installed by default on some distros (Fedora and Arch KDE worked out of the box). If you see dlopen(): error loading libfuse.so.2, install the right package:
| Distribution | Package | Install Command |
|---|---|---|
| Ubuntu 22.04 / Debian | libfuse2 | sudo apt install libfuse2 |
| Ubuntu 24.04+ | libfuse2t64 | sudo apt install libfuse2t64 |
| Fedora | fuse-libs | sudo dnf install fuse-libs |
| Arch Linux | fuse2 | sudo pacman -S fuse2 |
Sandbox note: The AppImage automatically disables Chromium's SUID sandbox (
--no-sandbox), because the AppImage's squashfs mount can't satisfy its permission requirements. This is standard for Electron AppImages and doesn't affect security.
.AppImage, .exe, or .dmg).asc)File → Decrypt/Verify Files... and select the downloaded .asc signature.This section covers the Docker/Podman platforms - Intel Mac, Windows, and Linux. (Apple Silicon Macs already have the server bundled in the app - see §2.1.)
The Server tab is laid out as a numbered wizard - just work through the cards from top to bottom:
Windows: make sure Docker Desktop is already running before you start - server setup fails if it isn't.
Notes:
~/.config/TranscriptionSuite/%APPDATA%\TranscriptionSuite\~/Library/Application Support/TranscriptionSuite/sudo nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml). Podman 4.7+ is required for podman compose.The default Docker image ships PyTorch built for Volta and newer GPUs (RTX 20-series and up). On a Pascal- or Maxwell-generation card the container crash-loops with an error like "NVIDIA GeForce GTX 1070 with CUDA capability sm_61 is not compatible with the current PyTorch installation".
Fix - switch to the CUDA Legacy image (built from the same Dockerfile but pinned to the cu126 PyTorch wheels, which still support these cards):
Once running, the legacy image behaves identically to the default. If you later move to a newer card, click the CUDA / CPU Only tile to switch back - staying on the legacy image with a modern GPU just gives you older PyTorch wheels for no benefit.
If you have an AMD or Intel GPU (instead of NVIDIA), you can get GPU-accelerated transcription with whisper.cpp and Vulkan. The setup differs per OS:
whisper-server.exe on your PC and manages it for you. See §2.7.1.Either way, you use GGML models (not the CUDA models) - see the model table below. Diarization is not available for GGML models. Note that on machines with an NVIDIA GPU, the app disables both Vulkan runtimes ("NVIDIA detected") and steers you to CUDA.
What you need:
How it works: the app downloads a native whisper-server.exe, runs it directly on Windows (not inside Docker), and the Docker backend talks to it at http://host.docker.internal:8080. Running it natively avoids the AVX2 CPU requirement of the containerized Vulkan build, so it works on a wider range of CPUs. Docker Desktop is still needed for the main backend container - only the whisper engine runs natively.
How to set it up:
whisper-server.exe automatically if it isn't already present, then starts the Docker backend; missing GGML weights download during startup.Where the file lives:
whisper-server.exeis stored at%APPDATA%\TranscriptionSuite\whisper-server\whisper-server.exeand managed automatically by the app. You don't need to install or configure it yourself.
What you need:
/dev/dri/renderD128 (a real DRI render node from the AMD/Intel kernel driver)How to set it up:
ggml-large-v3-turbo-q8_0.bin (see the model table below).Switching models: model selection is locked while the server is running. To switch, stop the server, pick the new model, then start again.
Recommended: ggml-large-v3-turbo-q8_0.bin (~1.4 GB) - best balance of speed, quality, and VRAM use for most AMD/Intel GPUs.
| Model | Size | Languages | Translation | Notes |
|---|---|---|---|---|
ggml-large-v3.bin | ~3.1 GB | 99 | Yes | Highest accuracy |
ggml-large-v3-q5_0.bin | ~2.1 GB | 99 | Yes | Good accuracy, lower VRAM |
ggml-large-v3-turbo.bin | ~1.6 GB | 99 | No | Fast, no translation |
ggml-large-v3-turbo-q5_0.bin | ~1.1 GB | 99 | No | Compact, fast |
ggml-large-v3-turbo-q8_0.bin | ~1.4 GB | 99 | No | Recommended |
ggml-medium.bin | ~1.5 GB | 99 | Yes | Good multilingual option |
ggml-medium-q5_0.bin | ~1.0 GB | 99 | Yes | Compact multilingual |
ggml-medium.en.bin | ~1.5 GB | English | No | English-only |
ggml-small.bin | ~465 MB | 99 | Yes | Lightweight |
ggml-small-q5_1.bin | ~370 MB | 99 | Yes | Smallest multilingual |
ggml-small.en.bin | ~465 MB | English | No | Smallest English-only |
Translation (to English) is available on the non-turbo, non-.en models. GGML weights download over HTTPS from huggingface.co/ggerganov/whisper.cpp.
iommu=soft to your kernel boot parameters.TranscriptionSuite supports remote transcription: a server machine (with a GPU) runs the container, and a client machine connects to it through the Dashboard app. Two connection profiles are available:
| Profile | Use Case | Network Requirement |
|---|---|---|
| Tailscale | Cross-network / internet (recommended) | Both machines on the same Tailnet |
| LAN | Same local network, no Tailscale needed | Both machines on the same LAN / subnet |
Both profiles use HTTPS + token authentication on port 9786. The only difference is how the client reaches the server and where the TLS certificates come from.
Remote profile chooser: the first time you click Start Remote without Tailscale certificates configured, a dialog asks you to choose between LAN and Tailscale. Pick LAN if both machines are on the same local network - no extra setup is needed (a self-signed certificate is generated automatically). Pick Tailscale for cross-network access. You can change this later in Settings → Client → Remote Profile.
┌─────────────────────────┐ HTTPS (port 9786) ┌─────────────────────────┐
│ Server Machine │◄────────────────────────────────►│ Client Machine │
│ │ + Auth Token │ │
│ • Runs the Dashboard │ │ • Runs the Dashboard │
│ • Clicks "Start Remote"│ Tailscale Tunnel │ • Settings → Client → │
│ • Has TLS certificates │ ── or ── │ "Use remote server" │
│ • Has the GPU │ LAN connection │ • No GPU needed │
└─────────────────────────┘ └─────────────────────────┘
Security model: Tailscale-profile traffic is reachable only from devices on your Tailnet; all traffic is TLS-encrypted; and every API request in remote mode requires a Bearer token. The Settings → Client tab also has a Manage Tokens panel to mint extra per-client tokens and revoke old ones.
Use this when the server and client are on different networks (e.g., home server ↔ work laptop), or when you want Tailscale's zero-config networking and automatic DNS.
Step 1 - Install & Authenticate Tailscale
sudo tailscale up (Linux) or via the Tailscale app (Windows/macOS)Your DNS settings should look like this:

Step 2 - Generate TLS Certificates (server machine only)
# Replace with your actual machine name + tailnet
sudo tailscale cert your-machine.your-tailnet.ts.net
This produces a .crt and a .key file. Move and rename them to the standard location so the app finds them without config changes (to change the location, edit remote_server.tls.host_cert_path / host_key_path in config.yaml):
Linux:
mkdir -p ~/.config/Tailscale
mv your-machine.your-tailnet.ts.net.crt ~/.config/Tailscale/my-machine.crt
mv your-machine.your-tailnet.ts.net.key ~/.config/Tailscale/my-machine.key
sudo chown $USER:$USER ~/.config/Tailscale/my-machine.*
chmod 600 ~/.config/Tailscale/my-machine.key
Windows (PowerShell):
mkdir "$env:USERPROFILE\Documents\Tailscale" -Force
mv your-machine.your-tailnet.ts.net.crt "$env:USERPROFILE\Documents\Tailscale\my-machine.crt"
mv your-machine.your-tailnet.ts.net.key "$env:USERPROFILE\Documents\Tailscale\my-machine.key"
For Windows, also update the certificate paths in config.yaml:
remote_server:
tls:
host_cert_path: "~/Documents/Tailscale/my-machine.crt"
host_key_path: "~/Documents/Tailscale/my-machine.key"
Note: Tailscale HTTPS certificates are issued for
.ts.nethostnames, so MagicDNS must be enabled in your Tailnet.Certificate expiry: these certificates expire after 90 days. The app attempts to auto-renew via
tailscale certbefore starting the server; if that fails, re-run thetailscale cert+mvcommands above.
Step 3 - Start the Server in Remote Mode
docker compose logs | grep "Admin Token:") - you'll need it on the client machine.Tailscale hostname: once the server is running, the Server tab displays the machine's full Tailscale FQDN (e.g.,
desktop.tail1234.ts.net) with a copy button. Use this exact hostname when configuring clients - not just the tailnet suffix.
Step 4 - Open the Firewall Port (Linux)
If the server machine runs a firewall, port 9786 must be open or connections will silently time out. The dashboard shows a warning banner on the Server tab if it detects the port may be blocked.
| Distribution | Command |
|---|---|
Ubuntu / Debian (ufw) | sudo ufw allow 9786/tcp comment 'TranscriptionSuite Server' |
Fedora (firewalld) | sudo firewall-cmd --permanent --add-port=9786/tcp && sudo firewall-cmd --reload |
my-machine.tail1234.ts.net) - copy it from the Server tab on the server machine. The Settings modal warns you if you enter a bare tailnet name without the machine prefix.9786 (Use HTTPS enables automatically).Tip: the client machine does not need certificates, Docker, or a GPU. It only needs Tailscale running and a valid auth token.
Use this when both machines are on the same local network and you don't want Tailscale - common for home-lab or office setups. LAN mode uses the same HTTPS + token authentication; only the hostname (a LAN IP instead of a .ts.net address) and the certificate source differ.
localhost and all detected LAN IPs. No manual steps are needed in most cases.
openssl command-line tool, which is preinstalled on Linux and macOS. On Windows, openssl.exe is usually not on PATH - if generation fails, install OpenSSL for Windows, make sure openssl.exe is on your PATH, and try again.config.yaml under remote_server.tls.lan_host_cert_path / lan_host_key_path (defaults: ~/.config/TranscriptionSuite/lan-server.crt / .key on Linux, ~/Documents/TranscriptionSuite/lan-server.crt / .key on Windows).sudo ufw allow 9786/tcp (Ubuntu/Debian) or sudo firewall-cmd --permanent --add-port=9786/tcp && sudo firewall-cmd --reload (Fedora).Same as the Tailscale client setup, except: select LAN as the remote profile and enter the server's LAN IP or hostname (e.g., 192.168.1.100) as the host.
Note on Kubernetes / custom deployments: if you run the server container directly (e.g., via Kubernetes or your own Docker setup), you can still use the LAN profile on the client - point the LAN host at your load balancer or service IP. The server image is at
ghcr.io/homelab-00/transcriptionsuite-server. EnsureTLS_ENABLED=trueand mount the certificate/key at/certs/cert.crtand/certs/cert.keyinside the container.
This is a summary - for more detail see README_DEV.
Mounted at /v1/audio/. These endpoints follow the OpenAI Audio API spec so that OpenAI-compatible clients (Open-WebUI, LM Studio, etc.) can point at TranscriptionSuite as a drop-in STT backend.
Auth: same rules as all other API routes - Bearer token required in TLS mode; open to localhost in local mode.
Error shape: errors follow the OpenAI error envelope:
{"error": {"message": "...", "type": "...", "param": null, "code": null}}
POST /v1/audio/transcriptionsTranscribe an audio or video file. Language auto-detected when language is omitted.
| Field | Type | Default | Description |
|---|---|---|---|
file | UploadFile | required | Audio or video file |
model | string | "whisper-1" | Accepted but ignored; the server uses whatever model is configured |
language | string | auto-detect | BCP-47 language code (e.g. en, fr) |
prompt | string | null | Initial prompt passed to the transcription engine as initial_prompt |
response_format | string | "json" | One of json, text, verbose_json, srt, vtt, diarized_json |
temperature | float | null | Accepted but ignored |
timestamp_granularities[] | list[string] | null | Include "word" to enable word-level timestamps (effective with verbose_json / diarized_json) |
diarization | bool | false | When true, run speaker diarization and attach speaker labels to segments |
expected_speakers | int (1-10) | null | Exact speaker count hint; out-of-range values return 400 |
parallel_diarization | bool | server config | Override parallel vs sequential diarize + transcribe for this call |
Response formats:
response_format | Content-Type | Shape |
|---|---|---|
json | application/json | {"text": "..."} - minimal OpenAI body; never leaks speaker labels |
text | text/plain | Raw transcript string |
verbose_json | application/json | Full OpenAI object (task, language, duration, text, segments, optional words); gains per-segment speaker and top-level num_speakers when diarization ran |
srt | text/plain | SRT subtitle file; cues prefixed Speaker 1:, Speaker 2: when diarization ran |
vtt | text/plain | WebVTT subtitle file; same speaker prefix as SRT |
diarized_json | application/json | Compact {task, language, duration, text, num_speakers, segments} with speaker, start, end, text per segment (raw SPEAKER_00 form for programmatic use) |
Speaker labels: JSON bodies (verbose_json, diarized_json) use raw SPEAKER_00/SPEAKER_01 form for stable programmatic identifiers. Subtitle formats (srt, vtt) normalize to Speaker 1/Speaker 2 - the same convention the dashboard's longform export uses.
Diarization failure tolerance: if diarization=true is requested but the diarization engine fails (no HF token, OOM, merge error), the endpoint returns 200 with a plain transcript (num_speakers=0, no speaker keys) and logs a WARNING server-side. Diarization hiccups never 5xx the call. Whenever diarization was requested, the response carries an X-Diarization-Status header (ready or unavailable) so clients can detect this.
Error codes:
| Status | type | Cause |
|---|---|---|
400 | invalid_request_error | Unknown response_format, empty file field, expected_speakers out of 1-10 |
429 | rate_limit_error | Another transcription job is already running - the job slot is shared with the dashboard, so a longform, import, or notebook job in progress also triggers this (Live Mode uses a separate path and doesn't occupy the slot) |
503 | server_error | No transcription model configured, or a backend dependency is unavailable |
500 | server_error | Internal engine error, or the job was cancelled |
A completely missing
filefield is rejected by FastAPI's own validation as a422with a different (non-OpenAI) error shape.
Diarized verbose transcript:
curl -X POST http://localhost:9786/v1/audio/transcriptions \
-H "Authorization: Bearer <token>" \
-F "file=@recording.wav" \
-F "diarization=true" \
-F "expected_speakers=2" \
-F "response_format=diarized_json"
Word-level verbose:
curl -X POST http://localhost:9786/v1/audio/transcriptions \
-H "Authorization: Bearer <token>" \
-F "file=@recording.wav" \
-F "response_format=verbose_json" \
-F "timestamp_granularities[]=word"
POST /v1/audio/translationsTranscribe and translate an audio or video file to English. Identical to /transcriptions (same fields, formats, and error codes) except:
language is not accepted (source language is always auto-detected)task field in verbose_json responses is "translate" instead of "transcribe"Backend note: translation requires a model with translation capability - a multilingual Whisper model (to English) or Canary v2 (bidirectional). Backends without translation support (Parakeet, SenseVoice, VibeVoice, turbo/
.enWhisper variants) return a400or500from the engine layer.
curl -X POST http://localhost:9786/v1/audio/translations \
-H "Authorization: Bearer <token>" \
-F "file=@foreign_audio.mp3" \
-F "response_format=text"
Diarized translation:
curl -X POST http://localhost:9786/v1/audio/translations \
-H "Authorization: Bearer <token>" \
-F "file=@foreign_audio.mp3" \
-F "diarization=true" \
-F "response_format=diarized_json"
TranscriptionSuite can send HTTP POST requests to an external URL whenever a transcription event occurs, so you can pipe results into your own applications, automation pipelines, or logging services.
| Event | Fires when |
|---|---|
live_sentence | A sentence is completed during Live Mode |
longform_complete | A transcription job finishes (file, import, notebook, or OpenAI-API job) |
Open Settings → Server tab. In the Outgoing Webhook section:
Authorization: Bearer <secret> on every requestThese settings are also editable directly in config.yaml:
webhook:
enabled: true
url: "https://your-api.example.com/webhook"
secret: "your-optional-secret"
Every webhook POST has Content-Type: application/json with this envelope:
{
"event": "live_sentence",
"timestamp": "2026-03-24T14:30:00.123456+00:00",
"payload": { ... }
}
Live sentence:
{
"source": "live",
"text": "The completed sentence."
}
Longform completion:
{
"source": "longform",
"text": "Full transcript text...",
"filename": "meeting.wav",
"duration": 1234.56,
"language": "en",
"num_speakers": 2
}
Test webhook (from the Send Test Webhook button): {"event": "test", ..., "payload": {"message": "Test webhook from TranscriptionSuite.", "source": "test"}}
Delivery notes:
.local/.internal hostnames, non-http(s) schemes) are blocked - the test button reports "URL blocked: targets a private or internal address". Use a publicly resolvable endpoint.As with most things, the first thing to try is turning it off and on again: stop the server/client, quit the app, and try again.
The next step is deleting things and letting the app re-fetch them. All the controls are in the Server tab:
What the volumes hold: data is your database and Notebook recordings (the irreplaceable one - back it up before clearing!), models is downloaded model weights (safe to clear, they re-download), and runtime is scratch space (always safe to clear; Clean All always wipes it). On Apple Silicon (Metal) there are no Docker volumes - the Persistent Volumes card lists the local directories instead.
Server logs live in the Logs tab in the sidebar - the first place to look when something misbehaves.
If the server crashes with CUDA failed with error unknown error after a system update (common on rolling-release distros like Arch), your NVIDIA driver likely updated past what the legacy Docker GPU hook supports. Switch to CDI mode:
sudo nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml
sudo nvidia-ctk config --in-place --set nvidia-container-runtime.mode=cdi
sudo systemctl restart docker
The dashboard detects CDI automatically and uses the correct GPU configuration - no image rebuild or reinstall needed. Note that the CDI spec describes your current driver, so re-run the cdi generate command after future driver updates too (the dashboard warns when the spec looks stale).
Symptom: the dashboard shows a red error state with "GPU unavailable" in the Session tab.
Steps:
sudo nvidia-smi -pm 1
sudo nvidia-smi --gpu-reset can reset the GPU without a full reboot, but this affects all processes using the GPU on the host.Symptom: nvidia-smi shows a healthy GPU, but the server logs report CUDA error 999 every time the container starts.
Cause: the NVIDIA driver can enter a degraded context when a container exits uncleanly. Without Persistence Mode, the driver resets incompletely on the next attach.
Solution: enable NVIDIA Persistence Mode on the host:
sudo nvidia-smi -pm 1
This persists until the next reboot. To make it permanent, install the included systemd unit (run from the repo root):
sudo cp build/nvidia-persistence.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now nvidia-persistence.service
Symptom: on a CPU-only machine (no NVIDIA GPU), first start fails during dependency install - either with invalid peer certificate: UnknownIssuer while downloading packages, or later with UnicodeEncodeError: 'latin-1' codec can't encode ... when loading the model.
Steps:
UnknownIssuer / certificate errors mean something (antivirus HTTPS scanning, or a corporate proxy) is intercepting HTTPS and re-signing it, so the container can't verify the package index. Turn off your antivirus's HTTPS-scanning setting, or point EXTRA_CA_CERTS_DIR at a folder holding the intercepting root CA - see the deployment guide.UnicodeEncodeError on model load means a HuggingFace token containing a non-ASCII character was provided. Clear the token (most models don't need one); the server also ignores non-ASCII tokens automatically and downloads anonymously.For more advanced troubleshooting steps, head over to README_DEV's Troubleshooting section.
For more information about the technical aspects of the project, check out README_DEV.
GNU General Public License v3.0 or later (GPLv3+) - See LICENSE.
This was initially developed as a personal tool and in time turned into a hobby project. I am an engineer, just not a software engineer; so this whole thing is vibecoded. At the same time it's not blind vibecoding; for example Dockerizing the server for easy distribution was 100% my idea.
I'm using this project to learn about programming. Starting from virtually nothing, I can now say that I've got a decent grasp of Python, git, uv & Docker. I started doing this because it's fun, not to make money. Though I do find, despite my mech eng degree, that I want to follow it as a career.
Anyways, since I'm 100% dogfooding the app I'm not going to abandon it (unless some other project makes mine completely redundant). I will also try to the best of my ability to deal with bugs as soon as possible.
Finally, I want to thank RealtimeSTT for inspiring this project.
I'm always open to contributors! Might help me learn a thing or two about programming.
To follow the progress of issues and planned features, head over to the project's Blackboard. Pick a planned feature to work on or add your own suggestion.
TypeScript
50.8%
Python
45.8%
JavaScript
1.5%
Shell
1.2%
A fully local and private Speech-To-Text app, offering multiple model backends, diarization & calendar mode - Available for Windows, macOS & Linux
720
stars
1,611
commits
TypeScript
primary language
Sep 5, 2026
updated
|
Turn speech into text - on your own computer.
TranscriptionSuite is a free, open-source transcription app. Record a lecture, dictate a document, or import an audio file, and get an accurate transcript with speaker labels in minutes. Everything runs locally: your audio never leaves your machine. Under the hood: an Electron dashboard + Python backend with multi-backend speech-to-text (Whisper, NVIDIA NeMo, VibeVoice-ASR, SenseVoice, whisper.cpp, MLX), accelerated by NVIDIA CUDA, Apple Metal, or AMD/Intel Vulkan - or plain CPU. Dockerized for fast setup. |
OS Support: Hardware Acceleration: |
TranscriptionSuite is two parts working as one app:
Day-to-day use is entirely point-and-click; a terminal is only needed for a few one-time setup steps on some platforms. Want to get going right away? Jump to Installation and pick your platform. Wondering whether your computer can run it? Check the Compatibility Matrix.
.txt), subtitles (.srt/.ass), or both, straight to a folder of your choice (no Notebook entry is created).📌Half an hour of audio transcribed in under a minute with Whisper (RTX 3060)!
*What downloads on first use: the STT model weights themselves, plus the PyAnnote diarization and wav2vec2 alignment models if you use them. Everything is cached locally and nothing further is fetched after that.
Not every model runs on every machine. Not sure what any of this means? You can skip this section - the app detects your hardware, pre-selects working defaults, and greys out incompatible choices with the reason shown.
A runtime is the engine mode the app uses to run the models, and it maps to your hardware: NVIDIA GPU = CUDA, Apple Silicon = Metal, AMD/Intel GPU = Vulkan, any machine = CPU. The tables below show which runtime fits your computer, and what each model family can do on it.
Where each runtime runs (runtime names as shown in the app):
| Runtime | Linux NVIDIA | Linux AMD/Intel | Windows NVIDIA | Windows AMD/Intel | macOS Apple Silicon | macOS Intel |
|---|---|---|---|---|---|---|
| CUDA (Docker) | Yes | No | Yes | No | No | No |
| CPU Only (Docker) | Yes | Yes | Yes | Yes | Yes¹ | Yes |
| Vulkan Linux (Docker + sidecar helper container) | No² | Yes | No | No | No | No |
| Vulkan Windows (Docker + native whisper-server) | No | No | No² | Yes | No | No |
| Metal (native, no Docker) | No | No | No | No | Yes | No |
¹ Possible, but Apple Silicon Macs should use Metal instead - it's native and much faster. ² When an NVIDIA GPU is detected, the app greys out the Vulkan runtimes ("NVIDIA detected" badge) and steers you to CUDA.
Models × runtimes × features:
| Model family | Runtime(s) | Live Mode | Translation | Diarization |
|---|---|---|---|---|
| Faster-Whisper (WhisperX) | CUDA, CPU | Yes | To English (not turbo/.en variants) | PyAnnote (token) |
| Parakeet v3 (NeMo) | CUDA | No | No | PyAnnote (token) |
| Canary v2 (NeMo) | CUDA | No | Yes - bidirectional, 25 languages | PyAnnote (token) |
| SenseVoice (FunASR) | CUDA, CPU (slow) | No | No | CAM++ built-in (no token) or PyAnnote |
| VibeVoice-ASR | CUDA, CPU (very slow) | No | No | Built-in |
| Whisper.cpp (GGML) | Vulkan Linux/Windows | Yes | To English (not turbo/.en variants) | No |
| MLX Whisper | Metal | Via faster-whisper (CPU) | To English (not turbo/.en variants) | Sortformer (≤ 4 speakers) or PyAnnote |
| MLX Parakeet v3 | Metal | Via faster-whisper (CPU) | No | Sortformer or PyAnnote |
| MLX Canary v2 | Metal | Via faster-whisper (CPU) | No (MLX port) | Sortformer or PyAnnote |
| MLX VibeVoice-ASR | Metal | Via faster-whisper (CPU) | No | Built-in |
Notes:
- Live Mode always runs on faster-whisper or whisper.cpp models. On Apple Silicon the Metal server bundles faster-whisper for exactly this - Live Mode works, but decodes on the CPU rather than the GPU.
- PyAnnote diarization needs a free HuggingFace token and accepting the model's terms; the app asks for the token during first-start setup. CAM++ (SenseVoice), Sortformer (Apple Silicon), and VibeVoice's built-in diarization need no token.
- Where diarization runs: file imports (Session tab > Import, Notebook tab > Import) and normal recordings on the Session tab, when the Speaker Diarization switch is on. Live Mode never diarizes - speaker attribution needs the whole recording, which a streaming transcriber does not have.
- Sortformer handles up to 4 speakers; pick PyAnnote on Apple Silicon for larger groups.
- NeMo models (Parakeet/Canary) are unavailable on the CPU runtime - the app substitutes a faster-whisper model instead.
- NeMo models can't output the Greek final sigma (ς), so Greek word endings may be truncated; the app shows a warning when this applies.
- VibeVoice-ASR is a 9-billion-parameter model (~16 GB download) - it wants a GPU with plenty of VRAM and is very slow on CPU.
- Model weights that are missing get downloaded automatically when the server starts - there is no separate download step.
- Older NVIDIA cards (GTX 10-series and earlier) use the same CUDA runtime with the CUDA Legacy image variant - see §2.6.
| Session Tab | Notebook Tab |
|---|---|
![]() | ![]() |
| Audio Note View | Server Tab |
|---|---|
![]() | ![]() |
The Releases page contains the Dashboard only - the server is downloaded from inside the app. Pick the section for your platform:
| Your computer | Section | How the server runs |
|---|---|---|
| Apple Silicon Mac (M1+) | § 2.1 | Natively on the Mac - no Docker |
| Intel Mac (pre-M1) | § 2.2 | Docker / Podman (CPU only) |
| Windows | § 2.3 | Docker / Podman |
| Linux | § 2.4 | Docker / Podman |
For Macs with an Apple chip (M1 and later). This is the easiest setup: the server runs natively on your Mac's GPU - no Docker, no Python, no separate server install.
Apple Silicon transcription runs on Apple's Metal + MLX GPU stack, bundled right inside the app. You download one file, drag it to Applications, and click Start Metal Server.
Naming note: wherever the app says "Metal server", what actually runs is MLX - Apple's machine-learning framework, which uses Metal as its GPU layer. MLX only exists on Apple Silicon, which is why Intel Macs (§2.2) can't use this path.
Prerequisite - FFmpeg: the server uses FFmpeg to decode compressed audio (M4A, MP3, FLAC, OGG - everything except WAV), and macOS doesn't ship it. Install it once with Homebrew:
brew install ffmpeg
Without it the server still starts, but importing anything other than a WAV file fails with a decode error. If you install FFmpeg while the app is running, restart the Metal server afterwards.
Install steps:
TranscriptionSuite-<version>-arm64-mac-metal.dmg (~3-5 GB) - the bundled build with the dashboard and the Python/MLX server pre-installed. (Careful: there is also a thin arm64-mac.dmg without the server - see the tip below.)xattr -dr com.apple.quarantine /Applications/TranscriptionSuite.app
Want to run the server somewhere else instead? If you'd rather use this Mac only as a remote control for a server on another machine (e.g. a PC with an NVIDIA GPU), or run the server locally in Docker, download the thin
arm64-mac.dmg(~200 MB, dashboard only) instead. Then set up a Docker server via §2.5-§2.6, or connect to another machine via §3 Remote Connection.
For older Intel-based Macs (pre-M1). The server runs in Docker on the CPU - it works, but expect it to be slow. Intel Macs get no GPU acceleration here, because Apple's MLX is Apple-Silicon-only.
x64-mac.dmg buildWindows runs the server in Docker (or Podman). With an NVIDIA GPU you get full acceleration; with an AMD or Intel GPU use the Vulkan path (§2.7); otherwise the server runs on CPU.
wsl --list --verbose in a terminal - the version column should read 2.Linux runs the server in Docker (or Podman). With an NVIDIA GPU you get full acceleration; with an AMD or Intel GPU use the Vulkan path (§2.7); otherwise the server runs on CPU.
Pick Docker or Podman - the app auto-detects whichever is installed (Docker is checked first).
Docker:
sudo pacman -S --needed dockerdocker group so the app can use Docker without sudo:
sudo usermod -aG docker $USER
Then log out and back in (or reboot) for it to take effect.sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
Skip this for CPU mode. (The app automatically prefers CDI when a CDI spec exists; if a later driver update breaks GPU access, see GPU not working after a system update.)Podman (4.7+ required for podman compose):
sudo pacman -S --needed podmansystemctl --user enable --now podman.socket
Without it, compose commands fail with "Cannot connect to the Docker daemon" even though podman itself works.sudo nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml
Skip this for CPU mode.Then continue with the shared steps: §2.5 Download the Dashboard app and §2.6 Setting Up the Server.
Download and install the Dashboard for your platform from the Releases page. This is just the frontend - no models or packages are downloaded yet - but it must be installed before you set up the server in §2.6.
Which file to download:
| Platform | File |
|---|---|
| Linux (x64) | TranscriptionSuite-<version>.AppImage |
| Windows (x64) | TranscriptionSuite Setup <version>.exe |
| Mac - Apple Silicon, bundled server | TranscriptionSuite-<version>-arm64-mac-metal.dmg (§2.1) |
| Mac - Apple Silicon, thin (dashboard only) | TranscriptionSuite-<version>-arm64-mac.dmg |
| Mac - Intel | TranscriptionSuite-<version>-x64-mac.dmg (§2.2) |
Only x64 builds exist for Linux and Windows - there are no ARM builds for those platforms. Each release file is signed with my GPG key (the matching
.ascfile); verifying is optional - see §2.5.2.
macOS only: after dragging the app into Applications, clear the "unverified app" quarantine once in Terminal:
xattr -dr com.apple.quarantine /Applications/TranscriptionSuite.app
AppImages need FUSE 2 (libfuse.so.2), which isn't installed by default on some distros (Fedora and Arch KDE worked out of the box). If you see dlopen(): error loading libfuse.so.2, install the right package:
| Distribution | Package | Install Command |
|---|---|---|
| Ubuntu 22.04 / Debian | libfuse2 | sudo apt install libfuse2 |
| Ubuntu 24.04+ | libfuse2t64 | sudo apt install libfuse2t64 |
| Fedora | fuse-libs | sudo dnf install fuse-libs |
| Arch Linux | fuse2 | sudo pacman -S fuse2 |
Sandbox note: The AppImage automatically disables Chromium's SUID sandbox (
--no-sandbox), because the AppImage's squashfs mount can't satisfy its permission requirements. This is standard for Electron AppImages and doesn't affect security.
.AppImage, .exe, or .dmg).asc)File → Decrypt/Verify Files... and select the downloaded .asc signature.This section covers the Docker/Podman platforms - Intel Mac, Windows, and Linux. (Apple Silicon Macs already have the server bundled in the app - see §2.1.)
The Server tab is laid out as a numbered wizard - just work through the cards from top to bottom:
Windows: make sure Docker Desktop is already running before you start - server setup fails if it isn't.
Notes:
~/.config/TranscriptionSuite/%APPDATA%\TranscriptionSuite\~/Library/Application Support/TranscriptionSuite/sudo nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml). Podman 4.7+ is required for podman compose.The default Docker image ships PyTorch built for Volta and newer GPUs (RTX 20-series and up). On a Pascal- or Maxwell-generation card the container crash-loops with an error like "NVIDIA GeForce GTX 1070 with CUDA capability sm_61 is not compatible with the current PyTorch installation".
Fix - switch to the CUDA Legacy image (built from the same Dockerfile but pinned to the cu126 PyTorch wheels, which still support these cards):
Once running, the legacy image behaves identically to the default. If you later move to a newer card, click the CUDA / CPU Only tile to switch back - staying on the legacy image with a modern GPU just gives you older PyTorch wheels for no benefit.
If you have an AMD or Intel GPU (instead of NVIDIA), you can get GPU-accelerated transcription with whisper.cpp and Vulkan. The setup differs per OS:
whisper-server.exe on your PC and manages it for you. See §2.7.1.Either way, you use GGML models (not the CUDA models) - see the model table below. Diarization is not available for GGML models. Note that on machines with an NVIDIA GPU, the app disables both Vulkan runtimes ("NVIDIA detected") and steers you to CUDA.
What you need:
How it works: the app downloads a native whisper-server.exe, runs it directly on Windows (not inside Docker), and the Docker backend talks to it at http://host.docker.internal:8080. Running it natively avoids the AVX2 CPU requirement of the containerized Vulkan build, so it works on a wider range of CPUs. Docker Desktop is still needed for the main backend container - only the whisper engine runs natively.
How to set it up:
whisper-server.exe automatically if it isn't already present, then starts the Docker backend; missing GGML weights download during startup.Where the file lives:
whisper-server.exeis stored at%APPDATA%\TranscriptionSuite\whisper-server\whisper-server.exeand managed automatically by the app. You don't need to install or configure it yourself.
What you need:
/dev/dri/renderD128 (a real DRI render node from the AMD/Intel kernel driver)How to set it up:
ggml-large-v3-turbo-q8_0.bin (see the model table below).Switching models: model selection is locked while the server is running. To switch, stop the server, pick the new model, then start again.
Recommended: ggml-large-v3-turbo-q8_0.bin (~1.4 GB) - best balance of speed, quality, and VRAM use for most AMD/Intel GPUs.
| Model | Size | Languages | Translation | Notes |
|---|---|---|---|---|
ggml-large-v3.bin | ~3.1 GB | 99 | Yes | Highest accuracy |
ggml-large-v3-q5_0.bin | ~2.1 GB | 99 | Yes | Good accuracy, lower VRAM |
ggml-large-v3-turbo.bin | ~1.6 GB | 99 | No | Fast, no translation |
ggml-large-v3-turbo-q5_0.bin | ~1.1 GB | 99 | No | Compact, fast |
ggml-large-v3-turbo-q8_0.bin | ~1.4 GB | 99 | No | Recommended |
ggml-medium.bin | ~1.5 GB | 99 | Yes | Good multilingual option |
ggml-medium-q5_0.bin | ~1.0 GB | 99 | Yes | Compact multilingual |
ggml-medium.en.bin | ~1.5 GB | English | No | English-only |
ggml-small.bin | ~465 MB | 99 | Yes | Lightweight |
ggml-small-q5_1.bin | ~370 MB | 99 | Yes | Smallest multilingual |
ggml-small.en.bin | ~465 MB | English | No | Smallest English-only |
Translation (to English) is available on the non-turbo, non-.en models. GGML weights download over HTTPS from huggingface.co/ggerganov/whisper.cpp.
iommu=soft to your kernel boot parameters.TranscriptionSuite supports remote transcription: a server machine (with a GPU) runs the container, and a client machine connects to it through the Dashboard app. Two connection profiles are available:
| Profile | Use Case | Network Requirement |
|---|---|---|
| Tailscale | Cross-network / internet (recommended) | Both machines on the same Tailnet |
| LAN | Same local network, no Tailscale needed | Both machines on the same LAN / subnet |
Both profiles use HTTPS + token authentication on port 9786. The only difference is how the client reaches the server and where the TLS certificates come from.
Remote profile chooser: the first time you click Start Remote without Tailscale certificates configured, a dialog asks you to choose between LAN and Tailscale. Pick LAN if both machines are on the same local network - no extra setup is needed (a self-signed certificate is generated automatically). Pick Tailscale for cross-network access. You can change this later in Settings → Client → Remote Profile.
┌─────────────────────────┐ HTTPS (port 9786) ┌─────────────────────────┐
│ Server Machine │◄────────────────────────────────►│ Client Machine │
│ │ + Auth Token │ │
│ • Runs the Dashboard │ │ • Runs the Dashboard │
│ • Clicks "Start Remote"│ Tailscale Tunnel │ • Settings → Client → │
│ • Has TLS certificates │ ── or ── │ "Use remote server" │
│ • Has the GPU │ LAN connection │ • No GPU needed │
└─────────────────────────┘ └─────────────────────────┘
Security model: Tailscale-profile traffic is reachable only from devices on your Tailnet; all traffic is TLS-encrypted; and every API request in remote mode requires a Bearer token. The Settings → Client tab also has a Manage Tokens panel to mint extra per-client tokens and revoke old ones.
Use this when the server and client are on different networks (e.g., home server ↔ work laptop), or when you want Tailscale's zero-config networking and automatic DNS.
Step 1 - Install & Authenticate Tailscale
sudo tailscale up (Linux) or via the Tailscale app (Windows/macOS)Your DNS settings should look like this:

Step 2 - Generate TLS Certificates (server machine only)
# Replace with your actual machine name + tailnet
sudo tailscale cert your-machine.your-tailnet.ts.net
This produces a .crt and a .key file. Move and rename them to the standard location so the app finds them without config changes (to change the location, edit remote_server.tls.host_cert_path / host_key_path in config.yaml):
Linux:
mkdir -p ~/.config/Tailscale
mv your-machine.your-tailnet.ts.net.crt ~/.config/Tailscale/my-machine.crt
mv your-machine.your-tailnet.ts.net.key ~/.config/Tailscale/my-machine.key
sudo chown $USER:$USER ~/.config/Tailscale/my-machine.*
chmod 600 ~/.config/Tailscale/my-machine.key
Windows (PowerShell):
mkdir "$env:USERPROFILE\Documents\Tailscale" -Force
mv your-machine.your-tailnet.ts.net.crt "$env:USERPROFILE\Documents\Tailscale\my-machine.crt"
mv your-machine.your-tailnet.ts.net.key "$env:USERPROFILE\Documents\Tailscale\my-machine.key"
For Windows, also update the certificate paths in config.yaml:
remote_server:
tls:
host_cert_path: "~/Documents/Tailscale/my-machine.crt"
host_key_path: "~/Documents/Tailscale/my-machine.key"
Note: Tailscale HTTPS certificates are issued for
.ts.nethostnames, so MagicDNS must be enabled in your Tailnet.Certificate expiry: these certificates expire after 90 days. The app attempts to auto-renew via
tailscale certbefore starting the server; if that fails, re-run thetailscale cert+mvcommands above.
Step 3 - Start the Server in Remote Mode
docker compose logs | grep "Admin Token:") - you'll need it on the client machine.Tailscale hostname: once the server is running, the Server tab displays the machine's full Tailscale FQDN (e.g.,
desktop.tail1234.ts.net) with a copy button. Use this exact hostname when configuring clients - not just the tailnet suffix.
Step 4 - Open the Firewall Port (Linux)
If the server machine runs a firewall, port 9786 must be open or connections will silently time out. The dashboard shows a warning banner on the Server tab if it detects the port may be blocked.
| Distribution | Command |
|---|---|
Ubuntu / Debian (ufw) | sudo ufw allow 9786/tcp comment 'TranscriptionSuite Server' |
Fedora (firewalld) | sudo firewall-cmd --permanent --add-port=9786/tcp && sudo firewall-cmd --reload |
my-machine.tail1234.ts.net) - copy it from the Server tab on the server machine. The Settings modal warns you if you enter a bare tailnet name without the machine prefix.9786 (Use HTTPS enables automatically).Tip: the client machine does not need certificates, Docker, or a GPU. It only needs Tailscale running and a valid auth token.
Use this when both machines are on the same local network and you don't want Tailscale - common for home-lab or office setups. LAN mode uses the same HTTPS + token authentication; only the hostname (a LAN IP instead of a .ts.net address) and the certificate source differ.
localhost and all detected LAN IPs. No manual steps are needed in most cases.
openssl command-line tool, which is preinstalled on Linux and macOS. On Windows, openssl.exe is usually not on PATH - if generation fails, install OpenSSL for Windows, make sure openssl.exe is on your PATH, and try again.config.yaml under remote_server.tls.lan_host_cert_path / lan_host_key_path (defaults: ~/.config/TranscriptionSuite/lan-server.crt / .key on Linux, ~/Documents/TranscriptionSuite/lan-server.crt / .key on Windows).sudo ufw allow 9786/tcp (Ubuntu/Debian) or sudo firewall-cmd --permanent --add-port=9786/tcp && sudo firewall-cmd --reload (Fedora).Same as the Tailscale client setup, except: select LAN as the remote profile and enter the server's LAN IP or hostname (e.g., 192.168.1.100) as the host.
Note on Kubernetes / custom deployments: if you run the server container directly (e.g., via Kubernetes or your own Docker setup), you can still use the LAN profile on the client - point the LAN host at your load balancer or service IP. The server image is at
ghcr.io/homelab-00/transcriptionsuite-server. EnsureTLS_ENABLED=trueand mount the certificate/key at/certs/cert.crtand/certs/cert.keyinside the container.
This is a summary - for more detail see README_DEV.
Mounted at /v1/audio/. These endpoints follow the OpenAI Audio API spec so that OpenAI-compatible clients (Open-WebUI, LM Studio, etc.) can point at TranscriptionSuite as a drop-in STT backend.
Auth: same rules as all other API routes - Bearer token required in TLS mode; open to localhost in local mode.
Error shape: errors follow the OpenAI error envelope:
{"error": {"message": "...", "type": "...", "param": null, "code": null}}
POST /v1/audio/transcriptionsTranscribe an audio or video file. Language auto-detected when language is omitted.
| Field | Type | Default | Description |
|---|---|---|---|
file | UploadFile | required | Audio or video file |
model | string | "whisper-1" | Accepted but ignored; the server uses whatever model is configured |
language | string | auto-detect | BCP-47 language code (e.g. en, fr) |
prompt | string | null | Initial prompt passed to the transcription engine as initial_prompt |
response_format | string | "json" | One of json, text, verbose_json, srt, vtt, diarized_json |
temperature | float | null | Accepted but ignored |
timestamp_granularities[] | list[string] | null | Include "word" to enable word-level timestamps (effective with verbose_json / diarized_json) |
diarization | bool | false | When true, run speaker diarization and attach speaker labels to segments |
expected_speakers | int (1-10) | null | Exact speaker count hint; out-of-range values return 400 |
parallel_diarization | bool | server config | Override parallel vs sequential diarize + transcribe for this call |
Response formats:
response_format | Content-Type | Shape |
|---|---|---|
json | application/json | {"text": "..."} - minimal OpenAI body; never leaks speaker labels |
text | text/plain | Raw transcript string |
verbose_json | application/json | Full OpenAI object (task, language, duration, text, segments, optional words); gains per-segment speaker and top-level num_speakers when diarization ran |
srt | text/plain | SRT subtitle file; cues prefixed Speaker 1:, Speaker 2: when diarization ran |
vtt | text/plain | WebVTT subtitle file; same speaker prefix as SRT |
diarized_json | application/json | Compact {task, language, duration, text, num_speakers, segments} with speaker, start, end, text per segment (raw SPEAKER_00 form for programmatic use) |
Speaker labels: JSON bodies (verbose_json, diarized_json) use raw SPEAKER_00/SPEAKER_01 form for stable programmatic identifiers. Subtitle formats (srt, vtt) normalize to Speaker 1/Speaker 2 - the same convention the dashboard's longform export uses.
Diarization failure tolerance: if diarization=true is requested but the diarization engine fails (no HF token, OOM, merge error), the endpoint returns 200 with a plain transcript (num_speakers=0, no speaker keys) and logs a WARNING server-side. Diarization hiccups never 5xx the call. Whenever diarization was requested, the response carries an X-Diarization-Status header (ready or unavailable) so clients can detect this.
Error codes:
| Status | type | Cause |
|---|---|---|
400 | invalid_request_error | Unknown response_format, empty file field, expected_speakers out of 1-10 |
429 | rate_limit_error | Another transcription job is already running - the job slot is shared with the dashboard, so a longform, import, or notebook job in progress also triggers this (Live Mode uses a separate path and doesn't occupy the slot) |
503 | server_error | No transcription model configured, or a backend dependency is unavailable |
500 | server_error | Internal engine error, or the job was cancelled |
A completely missing
filefield is rejected by FastAPI's own validation as a422with a different (non-OpenAI) error shape.
Diarized verbose transcript:
curl -X POST http://localhost:9786/v1/audio/transcriptions \
-H "Authorization: Bearer <token>" \
-F "file=@recording.wav" \
-F "diarization=true" \
-F "expected_speakers=2" \
-F "response_format=diarized_json"
Word-level verbose:
curl -X POST http://localhost:9786/v1/audio/transcriptions \
-H "Authorization: Bearer <token>" \
-F "file=@recording.wav" \
-F "response_format=verbose_json" \
-F "timestamp_granularities[]=word"
POST /v1/audio/translationsTranscribe and translate an audio or video file to English. Identical to /transcriptions (same fields, formats, and error codes) except:
language is not accepted (source language is always auto-detected)task field in verbose_json responses is "translate" instead of "transcribe"Backend note: translation requires a model with translation capability - a multilingual Whisper model (to English) or Canary v2 (bidirectional). Backends without translation support (Parakeet, SenseVoice, VibeVoice, turbo/
.enWhisper variants) return a400or500from the engine layer.
curl -X POST http://localhost:9786/v1/audio/translations \
-H "Authorization: Bearer <token>" \
-F "file=@foreign_audio.mp3" \
-F "response_format=text"
Diarized translation:
curl -X POST http://localhost:9786/v1/audio/translations \
-H "Authorization: Bearer <token>" \
-F "file=@foreign_audio.mp3" \
-F "diarization=true" \
-F "response_format=diarized_json"
TranscriptionSuite can send HTTP POST requests to an external URL whenever a transcription event occurs, so you can pipe results into your own applications, automation pipelines, or logging services.
| Event | Fires when |
|---|---|
live_sentence | A sentence is completed during Live Mode |
longform_complete | A transcription job finishes (file, import, notebook, or OpenAI-API job) |
Open Settings → Server tab. In the Outgoing Webhook section:
Authorization: Bearer <secret> on every requestThese settings are also editable directly in config.yaml:
webhook:
enabled: true
url: "https://your-api.example.com/webhook"
secret: "your-optional-secret"
Every webhook POST has Content-Type: application/json with this envelope:
{
"event": "live_sentence",
"timestamp": "2026-03-24T14:30:00.123456+00:00",
"payload": { ... }
}
Live sentence:
{
"source": "live",
"text": "The completed sentence."
}
Longform completion:
{
"source": "longform",
"text": "Full transcript text...",
"filename": "meeting.wav",
"duration": 1234.56,
"language": "en",
"num_speakers": 2
}
Test webhook (from the Send Test Webhook button): {"event": "test", ..., "payload": {"message": "Test webhook from TranscriptionSuite.", "source": "test"}}
Delivery notes:
.local/.internal hostnames, non-http(s) schemes) are blocked - the test button reports "URL blocked: targets a private or internal address". Use a publicly resolvable endpoint.As with most things, the first thing to try is turning it off and on again: stop the server/client, quit the app, and try again.
The next step is deleting things and letting the app re-fetch them. All the controls are in the Server tab:
What the volumes hold: data is your database and Notebook recordings (the irreplaceable one - back it up before clearing!), models is downloaded model weights (safe to clear, they re-download), and runtime is scratch space (always safe to clear; Clean All always wipes it). On Apple Silicon (Metal) there are no Docker volumes - the Persistent Volumes card lists the local directories instead.
Server logs live in the Logs tab in the sidebar - the first place to look when something misbehaves.
If the server crashes with CUDA failed with error unknown error after a system update (common on rolling-release distros like Arch), your NVIDIA driver likely updated past what the legacy Docker GPU hook supports. Switch to CDI mode:
sudo nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml
sudo nvidia-ctk config --in-place --set nvidia-container-runtime.mode=cdi
sudo systemctl restart docker
The dashboard detects CDI automatically and uses the correct GPU configuration - no image rebuild or reinstall needed. Note that the CDI spec describes your current driver, so re-run the cdi generate command after future driver updates too (the dashboard warns when the spec looks stale).
Symptom: the dashboard shows a red error state with "GPU unavailable" in the Session tab.
Steps:
sudo nvidia-smi -pm 1
sudo nvidia-smi --gpu-reset can reset the GPU without a full reboot, but this affects all processes using the GPU on the host.Symptom: nvidia-smi shows a healthy GPU, but the server logs report CUDA error 999 every time the container starts.
Cause: the NVIDIA driver can enter a degraded context when a container exits uncleanly. Without Persistence Mode, the driver resets incompletely on the next attach.
Solution: enable NVIDIA Persistence Mode on the host:
sudo nvidia-smi -pm 1
This persists until the next reboot. To make it permanent, install the included systemd unit (run from the repo root):
sudo cp build/nvidia-persistence.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now nvidia-persistence.service
Symptom: on a CPU-only machine (no NVIDIA GPU), first start fails during dependency install - either with invalid peer certificate: UnknownIssuer while downloading packages, or later with UnicodeEncodeError: 'latin-1' codec can't encode ... when loading the model.
Steps:
UnknownIssuer / certificate errors mean something (antivirus HTTPS scanning, or a corporate proxy) is intercepting HTTPS and re-signing it, so the container can't verify the package index. Turn off your antivirus's HTTPS-scanning setting, or point EXTRA_CA_CERTS_DIR at a folder holding the intercepting root CA - see the deployment guide.UnicodeEncodeError on model load means a HuggingFace token containing a non-ASCII character was provided. Clear the token (most models don't need one); the server also ignores non-ASCII tokens automatically and downloads anonymously.For more advanced troubleshooting steps, head over to README_DEV's Troubleshooting section.
For more information about the technical aspects of the project, check out README_DEV.
GNU General Public License v3.0 or later (GPLv3+) - See LICENSE.
This was initially developed as a personal tool and in time turned into a hobby project. I am an engineer, just not a software engineer; so this whole thing is vibecoded. At the same time it's not blind vibecoding; for example Dockerizing the server for easy distribution was 100% my idea.
I'm using this project to learn about programming. Starting from virtually nothing, I can now say that I've got a decent grasp of Python, git, uv & Docker. I started doing this because it's fun, not to make money. Though I do find, despite my mech eng degree, that I want to follow it as a career.
Anyways, since I'm 100% dogfooding the app I'm not going to abandon it (unless some other project makes mine completely redundant). I will also try to the best of my ability to deal with bugs as soon as possible.
Finally, I want to thank RealtimeSTT for inspiring this project.
I'm always open to contributors! Might help me learn a thing or two about programming.
To follow the progress of issues and planned features, head over to the project's Blackboard. Pick a planned feature to work on or add your own suggestion.
TypeScript
50.8%
Python
45.8%
JavaScript
1.5%
Shell
1.2%