KansaiGaijin aed6553ff4
All checks were successful
Build and Push / build (push) Successful in 12m35s
Serve original uploaded file instead of WAV for audio playback
The audio endpoint was serving the extracted WAV (16kHz mono PCM)
which balloons to ~1GB for a 3-hour session. Browsers cannot seek
efficiently in such a large uncompressed file.

Now the endpoint serves the original uploaded file (m4a, mp3, opus, etc.)
— a compressed format the browser can seek via byte-range requests.
Falls back to the WAV if the original is missing.
2026-07-21 22:04:02 +12:00

Nat20 Notes

Turn a recorded tabletop RPG session (audio or video) into two documents: a full GM/DM session log, and a spoiler-free player recap — using local transcription (WhisperX) and either a local LLM (Ollama) or any OpenAI-compatible hosted API for summarization.

Requirements

  • Docker + Docker Compose
  • An NVIDIA GPU with drivers + NVIDIA Container Toolkit installed on the host (for transcription; CPU-only works but is much slower)
  • A free HuggingFace token — needed for speaker diarization. You'll also need to accept the terms on the gated pyannote model page it links you to on first run.
  • Either: Ollama running somewhere reachable from this app (local or LAN), or an API key for a hosted LLM (OpenAI, or any OpenAI-compatible provider)

Quick start

git clone <this-repo>
cd nat20-notes
# Review docker-compose.yml — see Configuration below for env vars
docker compose up -d --build

Then open http://<your-server>:8020 (yes, that port reads "a d20") and follow the setup wizard:

  1. Choose a Whisper model size based on your GPU's available VRAM (guidance shown in-app)
  2. Choose local Ollama or a hosted API for summarization, and paste your HF token
  3. Optionally paste campaign/world context (NPC names, places) so summaries recognize them correctly

A reference compose file using named volumes only (no host paths) is at docker-compose.example.yml.

Using it

  1. Upload a recording (audio or video — video is auto-converted, audio-only files skip that step and are much faster). Files over 90 MB are auto-chunked with 3-way concurrent uploads.
  2. Transcribe — runs in the background. On completion you're automatically taken to the speaker-naming screen.
  3. Name your speakers — a waveform-style "session reel" shows each detected speaker's segments; click any point to jump the audio there and hear who's talking, then type in their name. Use the checkboxes to merge speakers (e.g. when diarization over-splits one person into SPEAKER_00, SPEAKER_05, etc.).
  4. Generate notes — click "Done naming" and confirm; notes generation starts automatically and you're taken to the job progress screen. When finished, navigate to the notes viewer.
  5. Review & tweak — regenerate notes, rename speakers, or delete jobs/sessions from the session detail page.

Data layout

The app stores everything under /data (inside the container):

Directory / File Contents
campaigns/{id}/audio/ Uploaded recordings and extracted audio, organised per campaign
campaigns/{id}/transcriptions/ Per-session transcript JSON files
campaigns/{id}/notes/ Generated notes (GM log + player recap)
app.db SQLite database (sessions, speakers, campaigns, settings, jobs)

Configuration

Environment variables (NAT20_*)

Set these under the backend service in docker-compose.yml to prefill the setup wizard and override defaults. All are optional — the wizard and Settings page can set them at runtime.

Transcription

Variable What it does Values Default Notes
NAT20_WHISPER_MODEL Whisper model size tiny base small medium large-v3 medium Larger = more accurate but uses more VRAM. medium fits most 6-8 GB GPUs; large-v3 needs ~10 GB+
NAT20_WHISPER_COMPUTE_TYPE Compute precision int8 float16 float32 int8 int8 = fastest / least VRAM. float16 = more accurate, more VRAM. float32 = full precision, slowest
NAT20_HF_TOKEN HuggingFace token for speaker diarization hf_... (none) Required for speaker attribution. Must accept pyannote gated-model terms with the same account first

LLM backend

Variable What it does Values Default Notes
NAT20_OLLAMA_HOST Ollama server URL URL http://localhost:11434 Set to your Ollama host
NAT20_OLLAMA_MODEL Ollama model name any model on your server qwen2.5:7b
NAT20_API_BASE_URL OpenAI-compatible API base URL https://api.openai.com/v1 Uncomment and set to switch from Ollama
NAT20_API_KEY API key string (none)
NAT20_API_MODEL API model name string gpt-4o-mini

Summarization

Variable What it does Values Default Notes
NAT20_CHUNK_WORD_TARGET Target words per summarisation chunk number 2500 Long transcripts are split into chunks, each summarised separately, then combined. Lower = more LLM calls but finer granularity (good for models with small context windows). Higher = more context per chunk but may exceed the model's window. 2500 is safe for most models (8K128K context)
NAT20_WORLD_CONTEXT Campaign context string text (none) Injected into every summarisation prompt so the LLM recognises NPCs, places, and lore correctly
NAT20_WORLD_CONTEXT_PATH Path to campaign context file inside container container path (none) Alternative to NAT20_WORLD_CONTEXT for large campaign bibles you update independently
NAT20_PLAYER_RECAP_STYLE Player recap format story diary bullets custom story
NAT20_PLAYER_RECAP_CUSTOM_PROMPT Custom prompt (only when style is custom) text (none)

Setup wizard

Variable What it does Values Default Notes
NAT20_ONBOARDING_COMPLETED Skip the setup wizard true or false false Set to "true" after you finish the wizard once

Campaign context

You can paste context directly in the Settings page, or point to a file inside the container using NAT20_WORLD_CONTEXT_PATH. The file version is useful for large campaign bibles that you update independently.

Notes on hardware

Transcription is GPU-bound and by far the slowest step for long sessions. Summarization is comparatively light — a 7-8B parameter local model is sufficient for most groups; only step up in size if you have the VRAM headroom after Whisper's footprint (they don't run at the same time, so you only need enough VRAM for whichever is currently running, plus normal system overhead from other GPU-using services).

Architecture

  • backend/ — FastAPI, SQLite (no external DB needed), WhisperX as a library (no nested Docker), in-process ThreadPoolExecutor background jobs (no Redis/Celery)
  • frontend/ — React 18 + Vite + Tailwind, served via nginx (listens on port 8020) which proxies /api to the backend

Both run as standard Docker Compose services — no special orchestration needed beyond GPU passthrough for the backend.

Description
AI audio transcription and auto-summarisation program
Readme MIT 302 KiB
Languages
TypeScript 51.4%
Python 46.3%
Makefile 0.7%
Dockerfile 0.5%
CSS 0.4%
Other 0.7%