Skip to content

Set up transcription

Sync between an ebook and its audiobook only works once the audiobook has been transcribed and the transcript aligned to the ebook text. This page covers picking a transcription route, deploying it, and what to expect once jobs are running.

Before you start

  • A Tandem server already installed and running — see Install the server.
  • Somewhere to transcribe: either a CUDA GPU host on the same network (the Jetson Orin Nano 8 GB is the reference) or a server image built with the local Whisper stack, which is multi-GB and slow without a GPU.
  • Transcription is optional. Without it you still get a library, an EPUB reader and an audio player — you just do not get cross-format position sync.

Steps

1. Choose a route

Two options, and the remote worker is the recommended one:

  • Remote GPU worker — a separate host, on the same LAN or VPN, running the Jetson transcription server. Set TRANSCRIPTION_PROVIDER=remote (or leave the default remote_with_fallback, which behaves the same on a server image with no local Whisper).
  • Local Whisper, built into the server image itself:

bash docker compose build --build-arg INSTALL_LOCAL_WHISPER=1

The torch + openai-whisper CUDA wheels this pulls in are multi-GB, and without a GPU on the server host, transcription runs on CPU and is slow.

The default server image ships with no local Whisper stack at all, so remote_with_fallback on that image has nothing to fall back to — it behaves as remote and retries the worker instead of erroring on the fallback leg. Use remote explicitly unless you have built with INSTALL_LOCAL_WHISPER=1.

The worker is a separate checkout on its own host — it needs only the jetson/ directory:

git clone --filter=blob:none --sparse https://github.com/jlafuenti/Book-Sync.git tandem-jetson
cd tandem-jetson
git sparse-checkout set jetson
cd jetson
cp docker-compose.example.yml docker-compose.yml

Generate the shared API key from the main server's System → Transcription Settings → Remote Server API Key, then paste it into the worker's compose file — the two are compared byte for byte, so they must match exactly:

# jetson/docker-compose.yml
environment:
  - TRANSCRIPTION_API_KEY=<paste the key here>

Deploy:

docker compose up -d --build

First boot downloads the Whisper model, so it can take a few minutes.

3. Point the server at the worker

Back in Tandem, under System → Transcription Settings:

  • Remote URL: http://<worker-lan-address>:9000
  • Remote Server API Key: the same value from step 2
  • Click Test Connection — it should report GPU and model status. "Model: Not Loaded" is normal for an idle worker; a 401 means the two keys do not match.

4. Queue a book

Queue a pair from the Transcription page, or turn on auto-transcribe so newly matched pairs queue themselves. One job runs at a time; the rest wait as pending.

5. Set expectations, and an off-hours window if you want one

There are no published timing numbers — transcription time depends on device (GPU vs CPU), model size and audio length, so time your first real book and extrapolate from there.

If the GPU is shared with something else, restrict dispatch to a window from System → Transcription Settings → Only transcribe during off-hours — a running job pauses at its next checkpoint when the window closes, and resumes without losing progress or burning a retry.

6. Fix a poor alignment

If a book's transcript came out wrong in places, open it from the Transcription page's editor and correct it there — re-alignment reads the edited transcript, so a bad stretch does not need a multi-hour retranscription to fix.

7. Keep the worker in step with the server

The System page's Updates card flags when the worker falls behind the server's version. Upgrade it the way you deployed it — check out the matching tag on the worker host and rebuild:

git fetch --tags && git checkout v<version>
docker compose up -d --build

Why it is like this

A separate GPU worker is the recommended route because the default server image carries no local model at all. The torch + openai-whisper CUDA wheels a local install needs are multi-GB, so they are opt-in at build time. A dedicated worker also keeps transcription load off the server's own CPU and memory, and it can be a small always-on box like an Orin Nano rather than a full workstation.

One job runs at a time, and a book takes hours, because the queue is deliberately serial. There is no published per-book number because the honest answer depends on your hardware — device and model size dominate, and audio length scales roughly linearly with cost. Time your first book on your own setup rather than trusting someone else's figure.

The worker requires a shared API key on every request, including its own health check, because anyone who can reach its port could otherwise submit jobs or read other users' cached transcripts. The key is checked byte for byte, which is why Test Connection exists: a mismatch shows up immediately as a 401 rather than as a mysterious failure hours into a job.

The off-hours window exists because a GPU is often shared with something else on the same box. Pausing a running job at its next checkpoint, rather than simply refusing new work, is what keeps a job that is already most of the way through a long book from losing progress when the window closes underneath it.

The transcript editor exists because alignment is fuzzy matching, not a guarantee. A stretch of audio Whisper mis-transcribed — background music, a foreign-language epigraph — throws off the sentence-level match for that whole stretch, and the fix is to correct the text, not to re-run a multi-hour job.

Go deeper