Exult TTS: Ultima VII conversations, books and scrolls read aloud

A patch for the Exult engine that speaks what the characters say — prefixed with who is saying it — and reads books and scrolls, using Piper or any speech command you like.

Patch on GitHub Exult

This is for you if:

What the patch does

Conversations

Each page of an NPC's speech is spoken as it appears on screen. Clicking on to the next page cuts the voice off and starts the next page, so you never wait for a long line to finish. The first page is spoken with the NPC's name prefix (Iolo: ...); nameless NPCs get no prefix, and the Avatar's lines carry the name you chose.

Exult's conversation code only knows the face it is showing, not who owns it. The name is resolved one level up, where usecode shows an NPC's face, and passed down with the face.

Books, scrolls and signs

The visible pages are read when a book or scroll opens and each time you turn the page. Exult repaints these windows often, so a page that was already spoken is not repeated. Rune signs, plaques and gravestones are also expanded and read aloud when examined. Closing the window stops the voice.

Clean text

The game's text carries markup meant for its fonts: * for a page break, ~, ^ for capitals, @ for quote marks, and font-mapped bytes. These are turned into plain sentences before they reach the speech engine.

A history in the terminal

Every line is printed to standard output as TTS: <line>, voice on or off. Start Exult from a terminal and you get a readable transcript; exult | tee talk.log keeps it.

An on/off switch

The Audio options screen gets a three-way Read aloud toggle (No / Yes / Yes + signs), saved in exult.cfg. It says "Read aloud (no command)" if the speech command cannot be found.

Safe with any game text. The speech command is started with posix_spawn, not a shell, so quotes and symbols in the text cannot break it. It runs in its own process group and is stopped by signalling that group — no other speech programs on your system are touched.

Build Exult with the patch

The patch is against upstream Exult commit 97984e9e0cbb4ccacaab0249da9c51b533818f74 (2026-10-29). It uses POSIX process calls; on other platforms the voice is simply off.

git clone https://github.com/exult/exult.git
cd exult
git checkout 97984e9e0cbb4ccacaab0249da9c51b533818f74
git apply /path/to/exult-tts/exult_tts.patch
autoreconf -i && ./configure && make
sudo make install

Set up the voice

Exult runs the command in config/audio/tts/command with the line as its only argument. By default that is tts_wrapper in Exult's data directory (/usr/local/share/exult after the install above). Any program that speaks its first argument and exits will do.

The included tts_wrapper uses Piper:

  1. Unpack a Piper binary from the Piper releases into ~/.local/share/piper/.
  2. Download a voice — the .onnx and its .onnx.json — from piper-voices into ~/.local/share/piper/models/. The default is en_GB-jenny_dioco-medium.
  3. Install the wrapper: sudo install -m 755 tts_wrapper /usr/local/share/exult/tts_wrapper.
variabledefault
EXULT_TTS_PIPER_DIR~/.local/share/piper
EXULT_TTS_PIPER$EXULT_TTS_PIPER_DIR/piper
EXULT_TTS_MODELen_GB-jenny_dioco-medium
EXULT_TTS_VOICE$EXULT_TTS_PIPER_DIR/models/$EXULT_TTS_MODEL.onnx
EXULT_TTS_HISTORYunset; a file to append every spoken line to

At startup Exult logs TTS: using <path>, or TTS: <path> not executable, voice off.

Tested on

Ubuntu 26.04.1 LTS with Exult 97984e9 and Ultima VII game data: conversations read aloud with the speaker's name, and the Read aloud switch in Audio options. The patch applies cleanly to a fresh checkout of that commit and builds, and the included standalone test (make check) covers the name prefix, markup cleanup, cut-off and book repaint handling.

Books and scrolls were not tried in game; they are covered only by make check. The sudo make install layout above was not itself run; the game was played from a local build with the wrapper in its data directory.

Not affiliated with the Exult team and not submitted upstream. The patch carries Exult's licence (GPLv2).