Preview
demo-video.mp4
- 1. Introduction
- 2. Installation
- 3. Remote Connection
- 4. OpenAI-compatible API Endpoints
- 5. Outgoing Webhooks
- 6. Troubleshooting
- 7. Technical Info
- 8. License
- 9. State of the Project
TranscriptionSuite is two parts working as one app:
- the Dashboard - the desktop program you install, and
- the server - the transcription engine, which the Dashboard downloads and manages for you (in Docker on most platforms, natively on Apple Silicon Macs). Docker is a free program that runs the server in its own self-contained box; you install it once during setup and the Dashboard drives it for you.
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.
- 100% local and private - everything runs on your own computer. Internet is only needed to download the app and the model weights on first use*; after that it works fully offline. Your audio and transcripts never leave your machine.
- Longform transcription - record for as long as you want, from your microphone or the system audio, and get the full transcript seconds after you stop. While you record, a rolling preview shows the latest ~20 seconds of transcription so you can watch it work.
- Live Mode - real-time, sentence-by-sentence transcription for continuous dictation workflows. Runs on Whisper (faster-whisper) and whisper.cpp models; other model families don't serve Live Mode.
- Speaker diarization - automatic "who said what" labels for Whisper, NeMo, SenseVoice, and VibeVoice models. Whisper and NeMo use PyAnnote (needs a free HuggingFace account token; the app walks you through it during setup); SenseVoice ships a built-in CAM++ diarizer and VibeVoice diarizes by itself (no token for either). On Apple Silicon, Sortformer provides Metal-native diarization for up to 4 speakers, no token needed.
- File import - drop existing audio or video files into the Session tab and save the result as plain text (
.txt), subtitles (.srt/.ass), or both, straight to a folder of your choice (no Notebook entry is created). - Your choice of speech models - on the Docker platforms (Linux / Windows / Intel Mac): WhisperX (faster-whisper models), NVIDIA NeMo Parakeet v3 / Canary v2, VibeVoice-ASR, SenseVoice (FunASR), and whisper.cpp (GGML models, whisper.cpp's compact model format, for AMD/Intel GPUs via Vulkan; see §2.7). On Apple Silicon: MLX Whisper (tiny → large-v3-turbo), MLX Parakeet v3, MLX Canary v2, and MLX VibeVoice-ASR, all running natively on the GPU without Docker.
- Truly multilingual - Whisper transcribes 90+ languages and can translate foreign audio to English; Canary v2 additionally translates in both directions across 25 European languages; Parakeet covers 25 European languages; VibeVoice 51 languages; SenseVoice 5 (Chinese, English, Cantonese, Japanese, Korean).
- Parallel processing - if your GPU has enough memory (VRAM), transcription and diarization run at the same time, cutting processing time significantly.
- Audio Notebook - every recording can be kept in a notebook with a calendar view, full-text search, and an AI assistant: chat about your notes with any OpenAI-compatible provider (LM Studio, Ollama, OpenAI, Groq, OpenRouter, and others).
- Global keyboard shortcuts - system-wide start/stop shortcuts and paste-at-cursor, so you can dictate into any app.
- Remote access - run the server on your desktop at home and transcribe from anywhere via Tailscale, or share it on your local network via LAN (§3).
- Stays out of your way - a Notifications entry at the bottom of the sidebar keeps a session log of downloads, server events, and errors (with toast popups for the important ones), and the app checks for new versions weekly so you don't have to.
- Plays well with others - an OpenAI-compatible API (§4) lets tools like Open-WebUI and LM Studio use TranscriptionSuite as their transcription engine, and outgoing webhooks (§5) push finished transcripts into your own automations.
📌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.
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 ffmpegWithout 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:
- From the Releases page, download
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 thinarm64-mac.dmgwithout the server - see the tip below.) - Open the DMG and drag TranscriptionSuite.app into Applications.
- The app is unsigned, so macOS will refuse to open it at first (as "unverified" or even "damaged"). Fix it either way:
- Terminal: run once:
xattr -dr com.apple.quarantine /Applications/TranscriptionSuite.app
- Or System Settings: try to open the app, then go to System Settings → Privacy & Security and click Open Anyway.
- Terminal: run once:
- Launch the app, open the Server tab, click the Metal tile in the 1. Runtime Settings card, then click Start Metal Server. The first start downloads your selected model (a multi-GB download), so give it a few minutes - after that you're ready to transcribe.
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.
- Install Docker. Install Docker Desktop for Mac (or Podman Desktop). The server will run in CPU mode - GPU acceleration isn't available through Docker on macOS.
- Download the thin dashboard and set up the server using the shared sections below:
- §2.5 Download the Dashboard app - use the
x64-mac.dmgbuild - §2.6 Setting Up the Server
- §2.5 Download the Dashboard app - use the
Windows 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.
- Install Docker Desktop with the WSL 2 backend. During installation, if you're given the choice, tick "Use WSL 2 instead of Hyper-V". You can confirm WSL 2 is active by running
wsl --list --verbosein a terminal - the version column should read 2. - NVIDIA GPU: install the NVIDIA GPU driver with WSL support (the standard NVIDIA gaming drivers work fine). Skip this if you'll run on CPU.
- AMD or Intel GPU: see §2.7 AMD / Intel GPU Support (Vulkan) - the recommended route for these cards.
- Then continue with the shared steps: §2.5 Download the Dashboard app and §2.6 Setting Up the Server.
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:
- Install Docker Engine.
- Arch:
sudo pacman -S --needed docker - Other distros: see the Docker documentation.
- Arch:
- Add your user to the
dockergroup so the app can use Docker withoutsudo:Then log out and back in (or reboot) for it to take effect.sudo usermod -aG docker $USER - NVIDIA GPU: install the NVIDIA Container Toolkit, then wire it into Docker and restart the daemon:
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.)
sudo nvidia-ctk runtime configure --runtime=docker sudo systemctl restart docker
Podman (4.7+ required for podman compose):
- Install Podman.
- Arch:
sudo pacman -S --needed podman - Fedora/RHEL: pre-installed.
- Other distros: see the Podman documentation.
- Arch:
- Enable the Podman API socket (needed for compose operations):
Without it, compose commands fail with "Cannot connect to the Docker daemon" even though
systemctl --user enable --now podman.socketpodmanitself works. - NVIDIA GPU: install the NVIDIA Container Toolkit (1.14+), then configure CDI by generating the device spec:
Skip this for CPU mode.
sudo nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml
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.
Verification steps
- Download both files from the same release:
- the installer/app (
.AppImage,.exe, or.dmg) - the matching signature file (
.asc)
- the installer/app (
- Install Kleopatra: https://apps.kde.org/kleopatra/
- Import my public key from this repository:
- In Kleopatra, use
File→Decrypt/Verify Files...and select the downloaded.ascsignature. - If prompted, select the matching app file. Verification should report a valid 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.
- Pick your runtime - in the 1. Runtime Settings card, click a tile: CUDA (NVIDIA GPUs), Vulkan Windows / Vulkan Linux (AMD/Intel GPUs, §2.7), Metal (Apple Silicon), or CPU Only (any machine). Incompatible tiles are greyed out with the reason; on machines with an NVIDIA GPU the app disables the other GPU runtimes with an "NVIDIA detected" badge (CPU Only stays available).
- Fetch the image - in the 2. Docker Image card, click Fetch Fresh Image. The Image Variant row switches automatically to match your runtime; the only choice you'd ever make there yourself is CUDA Legacy for older NVIDIA cards (see below).
- Choose your models - in the 3. Instance Settings card, pick a Main Transcriber, a Live Mode Model, and a Diarization engine by clicking their cards. Badges show what's Downloaded or Missing - anything missing is downloaded automatically when the server starts. Invalid combinations are greyed out with the reason (see the Compatibility Matrix).
- Start it - click Start Local at the bottom of the Instance Settings card. On the very first start the app walks you through a few quick prompts: confirming your model choices, approving the dependency download, and an Optional Diarization Setup step that asks for a HuggingFace token. The token is only needed for PyAnnote diarization - it's free (create one here with Read access) and you must also accept the model's terms. Skipping this just disables PyAnnote; CAM++ and Sortformer need no token.
- Wait for green. The first start downloads several GB and can take 10-20 minutes on reasonable hardware - later starts are fast. The status light turning green means the server is running and healthy.
- Start the client - go to the Session tab and click Start Local in the Client Link box. When it turns green, you're ready to roll!
Notes:
- Settings are saved to:
- Linux:
~/.config/TranscriptionSuite/ - Windows:
%APPDATA%\TranscriptionSuite\ - macOS:
~/Library/Application Support/TranscriptionSuite/
- Linux:
- Model selection is locked while the server is running - stop it first to switch models.
- GNOME: the AppIndicator extension is required for system-tray support.
- Docker vs Podman: both are supported and auto-detected (Docker first). If detection failed and you've since fixed the cause, use the Retry button in the Server tab's setup checklist. For GPU mode with Podman, make sure CDI is configured (
sudo nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml). Podman 4.7+ is required forpodman 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".
Affected cards
- GeForce GTX 10-series - 1050 / 1060 / 1070 / 1080 (and Ti variants)
- GeForce GTX 900-series and GTX 750 / 750 Ti
- Tesla P4 / P40 / P100 / M40
- Quadro P-series and M-series
Fix - switch to the CUDA Legacy image (built from the same Dockerfile but pinned to the cu126 PyTorch wheels, which still support these cards):
- In the 1. Runtime Settings card, make sure the runtime is CUDA.
- If the container already exists, stop the server and remove the container first.
- In the 2. Docker Image card, click the CUDA Legacy tile ("GTX 10-series / 900-series and older") in the Image Variant row. Confirm the "Switch to the CUDA Legacy image?" dialog and leave "Wipe runtime volume now (recommended)" checked, so the next bootstrap re-syncs the cu126 wheels cleanly.
- Click Fetch Fresh Image to pull the legacy image, then Start Local. The first start re-downloads PyTorch and dependencies (10-20 minutes); later starts are normal speed.
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:
- Windows - the app runs a native
whisper-server.exeon your PC and manages it for you. See §2.7.1. - Linux - a helper Docker container (sidecar) runs alongside the main one. See §2.7.2.
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:
- An AMD or Intel GPU with current Windows drivers
- Docker Desktop with the WSL 2 backend enabled (Settings → General → "Use the WSL 2 based engine") - without WSL 2 GPU passthrough the runtime tile is disabled with "Requires WSL2 GPU"
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:
- In the Server tab's 1. Runtime Settings card, click the Vulkan Windows tile. (It's marked Experimental in the app, but for AMD/Intel GPUs on Windows it's the recommended GPU path.)
- In the 2. Docker Image card, the Image Variant switches to Vulkan Windows automatically - this profile uses its own dedicated image. Click Fetch Fresh Image and wait for the download to finish.
- In the 3. Instance Settings card, select Whisper.cpp as your Main Transcriber and pick a GGML model from the row below it; optionally do the same for Live Mode. (Diarization isn't offered for GGML models - the selector says so itself.)
- Click Start Local. The app downloads
whisper-server.exeautomatically if it isn't already present, then starts the Docker backend; missing GGML weights download during startup. - Wait for the server status to turn green. You're ready to transcribe.
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:
- An AMD GPU with Vulkan support (RDNA1 or newer - e.g. RX 5500 XT, RX 6600, RX 7800 XT), or an Intel GPU with Vulkan support (Arc A-series or integrated Xe graphics)
- Docker installed (Podman isn't supported for Vulkan mode yet)
- A Linux host with
/dev/dri/renderD128(a real DRI render node from the AMD/Intel kernel driver)
How to set it up:
- In the 1. Runtime Settings card, click the Vulkan Linux tile.
- In the 2. Docker Image card, the Image Variant switches to Vulkan Linux automatically - this profile uses its own dedicated image. Click Fetch Fresh Image and wait for the download to finish.
- In the 3. Instance Settings card, under Main Transcriber select Whisper.cpp, then pick the GGML model you want from the row below it - e.g.
ggml-large-v3-turbo-q8_0.bin(see the model table below). - Under Live Mode Model, select Whisper.cpp again and pick a GGML model. Use the same model for Main and Live Mode - mixing different GGML models currently has issues we're still working on. (The "Same as main" shortcut isn't available for GGML models, so pick the same model manually in both selectors.)
- Click Start Local. Missing GGML weights download during startup; diarization isn't offered for GGML models.
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.
Full model table
| 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.
- A model is greyed out with a "Requires CUDA" badge - you're on a Vulkan runtime; use a GGML model instead (the CUDA models won't run through whisper.cpp).
- Sidecar health-check timeout - the model is still loading; large GGML files can take 30-60 seconds to initialize on first start.
- Older AMD GPUs (RDNA1): if you hit Vulkan initialization errors on an RX 5500 XT or similar RDNA1 card, try adding
iommu=softto your kernel boot parameters. - Windows feels slow? Make sure Docker Desktop is using the WSL 2 backend (Settings → General → "Use the WSL 2 based engine").
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
- Install Tailscale: tailscale.com/download
- Authenticate:
sudo tailscale up(Linux) or via the Tailscale app (Windows/macOS) - Go to the Tailscale Admin Console → DNS tab
- Enable MagicDNS and HTTPS Certificates
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.netThis 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.keyWindows (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
- Open the Dashboard on the server machine, go to the Server tab, and click Start Remote.
- Wait for the container to become healthy (green status).
- On the first remote start an admin auth token is generated automatically. Copy it from the Server tab's "Auth Token" field (or from the container logs:
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 |
- Install Tailscale on the client machine and sign in with the same account as the server machine (so both devices are on the same Tailnet).
- Open the Dashboard, go to Settings → Client, and enable "Use remote server instead of local".
- Select Tailscale as the remote profile.
- Enter the server's full Tailscale hostname (e.g.,
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. - Set port to
9786(Use HTTPS enables automatically). - Paste the auth token from the server, then close Settings - the client now connects to the remote server.
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.
- TLS certificate: the dashboard auto-generates a self-signed certificate on the first remote start if none exists, covering
localhostand all detected LAN IPs. No manual steps are needed in most cases.- Certificate generation uses the
opensslcommand-line tool, which is preinstalled on Linux and macOS. On Windows,openssl.exeis usually not on PATH - if generation fails, install OpenSSL for Windows, make sureopenssl.exeis on your PATH, and try again. - Custom certificate (optional): to use your own (e.g. from an internal CA), place it at the paths in
config.yamlunderremote_server.tls.lan_host_cert_path/lan_host_key_path(defaults:~/.config/TranscriptionSuite/lan-server.crt/.keyon Linux,~/Documents/TranscriptionSuite/lan-server.crt/.keyon Windows).
- Certificate generation uses the
- Start Remote: open the Server tab, click Start Remote, and copy the auth token once the container is healthy (same as Tailscale above).
- Firewall (Linux): if a firewall is active, open port 9786:
sudo ufw allow 9786/tcp(Ubuntu/Debian) orsudo 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}}Transcribe an audio or video file. Language auto-detected when language is omitted.
Form fields
| 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.
curl examples
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"Transcribe and translate an audio or video file to English. Identical to /transcriptions (same fields, formats, and error codes) except:
languageis not accepted (source language is always auto-detected)- Translation target is always English
- The
taskfield inverbose_jsonresponses 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 examples
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:
- Enable the webhook toggle
- Enter the URL to receive POST requests
- (Optional) Enter a Secret - sent as
Authorization: Bearer <secret>on every request - Save the server config (webhook fields are applied with the save button, not instantly), then click Send Test Webhook to verify your endpoint receives the request
These 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": { ... }
}Payload shapes per event
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:
- Delivery is fire-and-forget: each webhook is sent once with a 10-second timeout and is not retried on failure. Failed deliveries are logged server-side.
- For safety, URLs pointing at private or internal addresses (localhost, LAN IPs,
.local/.internalhostnames, 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:
- Remove Image - in the 2. Docker Image card
- Remove Container - in the 3. Instance Settings card
- Clear Data / Clear Models / Clear Runtime - per-volume buttons in the 5. Persistent Volumes card
- Clean All - the big red button in the 6. Clean Up card. Its dialog has checkboxes to keep the data volume, the models volume, and the config folder; everything unchecked means a full wipe.
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 dockerThe 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:
- Restart your computer to fully reset the GPU driver state. This resolves most cases of CUDA error 999 (driver context poisoning from a prior crash).
- If the error recurs frequently, enable NVIDIA Persistence Mode on the host to prevent the driver from entering a degraded state on container stop/start cycles:
sudo nvidia-smi -pm 1
- If the error persists after a reboot, check the Logs tab for the CUDA diagnostic line (logged at startup). It contains the torch version, CUDA version, and device nodes - useful for reporting the issue.
- Advanced:
sudo nvidia-smi --gpu-resetcan reset the GPU without a full reboot, but this affects all processes using the GPU on the host. - Workaround: switch to the CPU Only runtime in the Server tab while you investigate (expect slower transcription).
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 1This 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.serviceSymptom: 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:
- Use the CPU runtime. In the Server tab's Runtime Settings, select CPU Only before starting. CPU-only hosts then skip the multi-GB NVIDIA CUDA wheels and default to a lighter faster-whisper model.
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 pointEXTRA_CA_CERTS_DIRat a folder holding the intercepting root CA - see the deployment guide.UnicodeEncodeErroron 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.





