Inkwell is a Royal Scribe who rides alongside the party. The Scribe doesn't speak, doesn't roll, but after every session produces a chronicle of what happened — written as a diary entry from the Scribe's own perspective — and quietly updates the party's running records of the world they're shaping.
What Inkwell keeps track of:
- Session recaps — a narrative diary entry of the session, written as a chapter from the Scribe's journal
- Key decisions — major choices the party made (allegiances, refusals, bargains)
- World lore — places, factions, history, and any new details about the setting as they're revealed
- NPCs — names, descriptions, and roleplay summaries for everyone the party meets
- Enemies — creatures and adversaries encountered, separate from neutral or friendly NPCs
- Inventory & loot — items awarded to the party, rewards, and notable gear changes
The result is a living archive of the campaign that grows on its own — useful for catching up after a missed session, for the DM's continuity, and for feeding into other tools (e.g. NotebookLM) as source material.
The pipeline turns a raw multi-track Discord recording into a finished session chronicle. End to end:
- Record the session in Discord. Add the Craigbot recording bot to the party's Discord server and have it record the voice channel for the duration of the session. Craigbot produces one audio track per speaker.
- Download the recording. When the session ends, download the multi-track
.ziparchive from Craigbot. - Drop the zip into
recordings/. This is the single trigger for the rest of the pipeline. - Local transcription.
scribe_pipeline.pyunpacks the zip, transcribes each speaker's track withmlx-whisper, and interleaves all segments chronologically into a singletranscript_raw.mdso dialogue flows in real time across speakers. - Local LLM extraction.
extract_data.pyruns against a local Ollama instance:mistral-nemo:12breads the transcript in chunks and synthesizes Inkwell's diary entry;mistral:7bextracts a structuredsession_data.jsonwith decisions, NPCs, lore, loot, and allies. - Persist. The pipeline writes a dated
mm_dd_yyyy_recap.mdtorecaps/, and appends the new findings to the runninglore/world_lore.md,npcs/npcs.md, andallies/allies.md. - Archive the source. The original
.zipis moved intoarchive/(renamed to the session date) and the extracted audio is deleted to reclaim disk.
Each mm_dd_yyyy_recap.md written to recaps/ is structured as:
- A diary entry at the top — chapter-style narrative prose written from the Scribe's perspective, covering only in-game events
- A Scribe's Notes section at the bottom with:
- Key Decisions — major party choices (allegiances, refusals, bargains)
- Loot Found — items recovered in the field
- Purchases — items acquired from shops or merchants
Inkwell runs entirely on a single machine. Both transcription and LLM inference happen locally.
- Apple Silicon Mac —
mlx-whisperuses the MLX backend and requires Apple Silicon - Python 3.9+
- Packages listed in
requirements.txt:mlx-whisper,librosa,soundfile,pydub,pydantic,python-dotenv - Ollama running locally on port 11434
- Models pulled into Ollama:
mistral-nemo:12b(narrative) andmistral:7b(extraction)
Copy .env.example to .env. The only variable is:
| Variable | Purpose |
|---|---|
OLLAMA_HOST |
URL of the local Ollama service — defaults to http://localhost:11434 |
git clone git@github.com:hackswithcoffee/Inkwell.git
cd Inkwell
# Python environment
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
# Environment
cp .env.example .env
# Party roster — map your Discord usernames to character names
cp players.example.json players.json
# Edit players.json
# Install Ollama and pull the models
curl -fsSL https://ollama.com/install.sh | sh
ollama pull mistral-nemo:12b
ollama pull mistral:7b
# Sanity check — runs all the startup validators without doing real work
.venv/bin/python -c "import scribe_pipeline; print('Ready')"Data folders (recordings/, recaps/, lore/, npcs/, allies/, archive/) are created automatically on first run.
.venv/bin/python scribe_pipeline.py
# Or pin a session date instead of defaulting to today:
.venv/bin/python scribe_pipeline.py --date 05_03_2026The pipeline picks up the most recent .zip in recordings/ and processes that one.
- Size guard:
.zipfiles larger than 2GB are refused unless explicitly confirmed. - Source archival: After a successful run, the source
.zipis moved intoarchive/and renamed to the session date (mm_dd_yyyy.zip). Extracted audio intemp_audio/is deleted to reclaim disk. - Overlap handling: When speakers overlap during a session, both segments are preserved in the order they started — no truncation of cross-talk.
- Out-of-character filtering: Scheduling chatter, audio glitches, and fourth-wall breaks are filtered out at the LLM extraction step and do not appear in the recap.
- Real names: The recap and diary entry refer to players by character name only. Real names exist exclusively in
players.json(gitignored), which maps Discord usernames toCharacter (Real Name). Copyplayers.example.jsontoplayers.jsonto set up your party, and update it when the party composition changes.
scribe_pipeline.py— orchestrates the audio → transcription → extraction → persistence flowextract_data.py— calls Ollama to produce the diary entry and structured session datasummarizer_primer.md— D&D rules cheat sheet that grounds the summarizer's terminologydnd rules/— SRD reference content used by the primerrecordings/— drop new Craigbot.zipfiles hererecaps/,lore/,npcs/,allies/— generated and maintained artifactsarchive/— processed.zipfiles, renamed to the session date
The code in this repository is released under the MIT License.
The contents of dnd rules/ and any derived material in summarizer_primer.md are sourced from Wizards of the Coast's System Reference Document 5.2:
This work includes material from the System Reference Document 5.2 ("SRD 5.2") by Wizards of the Coast LLC, available at https://www.dndbeyond.com/srd. The SRD 5.2 is licensed under the Creative Commons Attribution 4.0 International License, available at https://creativecommons.org/licenses/by/4.0/legalcode.