Free, often faster than uploading, and the recording never leaves your machine. Set-up, the commands, and how the result gets into your campaign.
You can upload a recording and we will transcribe it for you. This page is the other way of doing it, and for a lot of tables it is the better one.
The honest cost: an afternoon of set-up the first time, and a machine you are willing to leave running. If that is not a trade you want to make, upload the recording instead and skip this page entirely — the result is the same transcript in the same place.
The model that matters is large-v3, and it needs about 4.7 GB of VRAM in float16. Any NVIDIA card with 6 GB or more runs it comfortably; 8 GB and up leaves room for speaker labelling later.
| Your machine | What to run | Roughly |
|---|---|---|
| NVIDIA GPU, 8 GB+ VRAM | large-v3, batched | 25–40× real time — a four-hour session in about 8 minutes |
| NVIDIA GPU, 6 GB VRAM | large-v3, standard | 10–20× real time — a four-hour session in 15–25 minutes |
| NVIDIA GPU, 4 GB VRAM | large-v3-turbo, or int8_float16 | Workable. Test the quality on your table first |
| Apple Silicon | whisper.cpp with Metal | Usable. Slower than a discrete GPU, far faster than a CPU |
| CPU only | whisper.cpp, a smaller model | 1–3× real time. A four-hour session is most of a day |
You will also need Python 3.11 — not 3.12 or 3.13, because the speaker-labelling dependencies lag behind — and ffmpeg on your PATH.
| Tool | What it gives you | Use it when |
|---|---|---|
| faster-whisper | Text with timestamps. Fast, simple, few dependencies | Start here. It is the whole job if you don't need speaker names |
| WhisperX | The above plus speaker labels — SPEAKER_00, SPEAKER_01… | You want to know who said what. Most DMs eventually do |
| whisper.cpp | CPU and Apple Silicon friendly, minimal install | No NVIDIA GPU |
# 1. An isolated environment, so none of this touches your system Python
py -3.11 -m venv C:\whisper-env
C:\whisper-env\Scripts\Activate.ps1
# 2. PyTorch with CUDA (check pytorch.org for the current cu-version)
pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu121
# 3. Confirm the GPU is actually visible - this must print True and your card
python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"
# 4. The transcriber
pip install faster-whisper
# 5. ffmpeg, if you don't have it
winget install ffmpegpython3.11 -m venv ~/whisper-env
source ~/whisper-env/bin/activate
pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu121
python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"
pip install faster-whisper
sudo apt install ffmpeg # or dnf / pacman, to tasteThere is no CUDA on a Mac, so faster-whisper runs on the CPU and is slow. Use whisper.cpp, which is built for Metal:
brew install whisper-cpp ffmpeg
# Fetch the model once (about 3 GB)
whisper-cpp-download-ggml-model large-v3
# Transcribe, writing an .srt beside the audio
whisper-cpp -m ggml-large-v3.bin -f session-07.wav -l en -osrtwhisper.cpp wants 16 kHz mono WAV. Convert first with ffmpeg -i session-07.m4a -ar 16000 -ac 1 session-07.wav — and it is worth doing on any platform, because speech needs very little bitrate and the file gets much smaller.Take one messy stretch of a real session: several people talking over each other, dice in the background, someone eating crisps. Not your clearest recording — your most typical one.
ffmpeg -i session-07.m4a -ss 00:20:00 -t 00:10:00 -c copy test10.m4aThen run both candidate models against it and read the output yourself:
from faster_whisper import WhisperModel
import time
AUDIO = r"test10.m4a" # the ten-minute clip you just cut
LANG = "en" # "el" Greek, "en" English, or None to auto-detect
for size in ["large-v3-turbo", "large-v3"]:
print(f"\n{'='*60}\n{size}\n{'='*60}")
model = WhisperModel(size, device="cuda", compute_type="float16")
t0 = time.time()
segments, info = model.transcribe(AUDIO, language=LANG, beam_size=5, vad_filter=True)
text = " ".join(s.text for s in segments)
dt = time.time() - t0
print(f"took {dt:.0f}s for {info.duration:.0f}s of audio -> {info.duration/dt:.1f}x real time")
print(text[:1500])
del modelThis is the only test that matters, and the thing being tested is your ear, not a number. Look for:
If large-v3-turbo is good enough, use it — it is roughly twice as fast. If it is not, and you have the VRAM, there is no reason to compromise.
initial_prompt feeds the model a list of words to expect. It is the difference between *Opal Silvermist* and *opal silver mist*, four hundred times over, and it is the single highest-value line in any of these scripts.
PROMPT = ("Whisperwood, Eldoria, Opal Silvermist, Kalvius Draxil, "
"Maren Oakbarrel, Elira Wren, Greyveil, Daskwood, Aqualora")
model.transcribe(AUDIO, language="en", initial_prompt=PROMPT, ...)Point this at the folder holding your recordings. It writes an .srt and a .txt beside each file, and skips anything already done, so you can stop it and restart it safely.
"""Transcribe every session recording in a folder.
Writes <name>.srt and <name>.txt beside each audio file.
Safe to re-run: files that already have an .srt are skipped."""
from faster_whisper import WhisperModel
from pathlib import Path
import time, sys
ROOT = Path(r"C:\campaign\recordings") # the folder holding your recordings
MODEL = "large-v3" # or "large-v3-turbo" if the test in section 5 said it's fine
LANGUAGE = "en" # "el" Greek - "en" English - None to auto-detect
AUDIO_EXT = {".m4a", ".mp3", ".wav", ".ogg", ".opus", ".flac", ".mp4", ".mkv"}
# Your world's proper nouns. See section 6 - this is the single highest-value line here.
PROMPT = "Whisperwood, Eldoria, Opal Silvermist, Maren Oakbarrel, Greyveil"
def ts(seconds: float) -> str:
h, rem = divmod(seconds, 3600); m, s = divmod(rem, 60)
return f"{int(h):02}:{int(m):02}:{int(s):02},{int((s%1)*1000):03}"
def main():
files = sorted(p for p in ROOT.rglob("*") if p.suffix.lower() in AUDIO_EXT)
if not files:
sys.exit(f"No audio found under {ROOT}")
print(f"{len(files)} file(s) found. Loading {MODEL}...")
model = WhisperModel(MODEL, device="cuda", compute_type="float16")
for i, audio in enumerate(files, 1):
srt = audio.with_suffix(".srt")
txt = audio.with_suffix(".txt")
if srt.exists():
print(f"[{i}/{len(files)}] skip (already done): {audio.name}")
continue
print(f"[{i}/{len(files)}] {audio.name}")
t0 = time.time()
segments, info = model.transcribe(
str(audio), language=LANGUAGE, beam_size=5,
vad_filter=True, vad_parameters={"min_silence_duration_ms": 500},
initial_prompt=PROMPT,
)
lines, plain = [], []
for n, seg in enumerate(segments, 1):
lines.append(f"{n}\n{ts(seg.start)} --> {ts(seg.end)}\n{seg.text.strip()}\n")
plain.append(seg.text.strip())
if n % 100 == 0:
print(f" ...{n} segments, {seg.end/60:.0f} min in", flush=True)
srt.write_text("\n".join(lines), encoding="utf-8")
txt.write_text("\n".join(plain), encoding="utf-8")
dt = time.time() - t0
print(f" done in {dt/60:.1f} min ({info.duration/dt:.1f}x real time)")
print("\nAll finished.")
if __name__ == "__main__":
main()C:\whisper-env\Scripts\Activate.ps1
python transcribe-campaign.pyThis is the step that turns a wall of text into *“Marina: …”*. It needs WhisperX, a free Hugging Face account, and a token you generate yourself.
pip install whisperxYou must also accept the terms for two pyannote models on their Hugging Face pages, then pass your own token:
whisperx session-07.m4a \
--model large-v3 --language en \
--diarize --min_speakers 4 --max_speakers 8 \
--hf_token YOUR_TOKEN_HERE \
--output_format srt --output_dir .SPEAKER_00 in session 3 is not the same person as SPEAKER_00 in session 4. Lore & Play remembers what you map each label to per campaign, so you name them once per file and the names stick.On 8 GB of VRAM or less you may need to let WhisperX load the transcription and speaker models one after the other rather than together. If it runs out of memory, that is the first thing to change.
Everything above produces files. Here is where each one goes.
.srt.SPEAKER_00 to a real name once and it is remembered for the rest of the campaign..srt files along with your written notes — same button, as many as you like.s07-2026-05-22.srt) and your sessions arrive dated and in the order you played them..srt carries the timing, and the timing is what lets the assistant find “the bit where they met the harbourmaster” instead of handing you four hours of prose.| What you see | Why | What to do |
|---|---|---|
torch.cuda.is_available() prints False | CPU-only torch, or an old driver | Reinstall torch with the right cu index and update the NVIDIA driver. Check this before an overnight run, not after |
| Very slow, GPU sitting idle | It fell back to the CPU | Same as above |
CUDA out of memory | Model too large, or speaker labelling loaded alongside it | Use compute_type="int8_float16", or run the speaker pass separately |
| Invented names come back mangled | Expected — the model has never seen them | Extend initial_prompt (section 6) |
| Long silences transcribed as invented speech | Whisper hallucinating on silence | vad_filter=True is already on; raise min_silence_duration_ms |
| Greek transcribed in Latin letters | Language auto-detected wrong | Set language="el" explicitly |
| A table that switches between two languages | One language is chosen per file | Transcribe twice and keep the better result, or split the audio |
The .srt won't import | Almost always an encoding problem | Make sure it is saved as UTF-8. The scripts here already do |
If the transcript is good but the import is not doing what you expect, that is our end rather than yours — the DM manual covers the transcript tools, and the campaign importer shows you everything it read before anything is saved.