World & rooms

Spoken lines

A voice mod makes people say their scene lines out loud, with the mood the scene gives them. Voices, moods and the narrator are data.

With a voice mod installed, every Game:Say line is spoken by the person who says it, in the mood the line gives ("Furious", "Crying", "Flirty"…), and the mouth follows the sound. Narration can be read too. Nothing changes in the scenes themselves: the voice mod reads the lines the game already shows.

The official one is ul_Voices, shipped as the launcher's optional Voices pack. It runs the Qwen3-TTS speech model on the player's graphics card; the launcher recommends it from 8 GB of video memory. Below that it offers ul_VoicesLite (Voices (light)): Kokoro on the processor, real time anywhere but flat, with no moods and adult voices only. With both installed, ul_Voices is used.

modder

A mod can add voices, rewrite how a mood is acted, give the narrator another voice, or pick a character's voice from a scene. All of it is JSON under Voice/.

What happens to a line

  1. A scene shows a line. The engine picks the speaker's voice and the instruction for the line's mood.
  2. The speech model renders the line to a sound file (about a fifth of the line's length on a recent graphics card).
  3. The clip plays from the person's head on the Voice bus, and the lips move to it. Narration and phone calls play without a mouth; a phone call (or a thought, any secondScreen speaker) goes through a handset filter. Some moods start with a sound first, a sob before a crying line or a laugh before a happy one (see Fillers); the line follows when it ends.
  4. In Auto pacing the scene waits for the end of the line. A click stops the voice and moves on. In Skip nothing is spoken.

A line that cannot be rendered in time stays silent: a voice never holds a scene up. Rendered lines are kept (Saved/VoiceCache, 500 MB at most, the lines played longest ago are removed first), so a line said again plays at once.

Rendering lines ahead of time

A line is rendered when it is first said, and a long line on a slow card is a pause the player hears. When a scene knows its lines in advance (a cinematic, a trailer, a mod's own stock of fillers), it can have them rendered into the cache before they are needed:

luaGame:prerenderVoice(landlord, "Angry", "Friday. Not next Friday. This one.")
Game:prerenderVoice(nil, nil, "I came here with a hundred and fifty dollars.")  -- the narrator
while Game:voicePrerenderPending() > 0 do Game:Hold(1) end                      -- optional
  • The speaker, the mood and the text must be exactly what Game:Say / Game:Narrate will use: the cache key is the voice, the mood's instruction and the cleaned text. A different mood is a different clip.
  • Nothing plays. The clip lands in Saved/VoiceCache like a spoken line and is then found at once; a line already there costs nothing.
  • Cache renders queue one at a time behind any live line, and a live line is never held back or cancelled by them. Game:voicePrerenderPending() is how many are still to come.
  • Without a voice mod, or with voices off in the settings, the call does nothing.

Who gets which voice

  • The player speaks with one of the model's own built-in speakers (PlayerMale / PlayerFemale below).
  • Everyone else keeps one voice for life: the engine picks among the voices whose sex and age range fit the person, the same one every time.
  • A scene can choose it: Actor:setVar("voice", "<voice id>"). The choice stays with the person.
  • A voice is never given across the line of 18: a child's voice is only for a child, an adult's only for an adult. A person no voice fits stays silent.

Files

<mod>/Voice/
  Engine.ulvoiceengine   the speech process and its model (the voice mod brings it)
  Moods.ulvoicemood      how each mood is acted, and which sound may come first
  Voices/*.ulvoice       one file per voice
  Fillers/<voice id>/    sobs, sighs, laughs... in that voice (.mp3, .wav or .flac)

Voices and moods add up across mods: a later mod replaces a voice with the same Id, or a mood of the same name. A mod can ship only Voices/ or only Moods.ulvoicemood and rely on ul_Voices for the engine.

.ulvoice

json{
	"Id": "elder_f_03",
	"Label": "Old woman, soft and dry",
	"Sex": "f",
	"AgeMin": 75,
	"AgeMax": 200,
	"Embedding": "<base64 of 1024 little-endian floats>"
}
  • Sex: "m" or "f", matched against the person's gender.
  • Engine: the speech engine the voice is made for, "qwen" when left out, "kokoro" for the light mod. Only voices made for the installed engine are used.
  • AgeMin / AgeMax: the ages the voice fits. Several voices can share a range; people are spread over them.
  • Embedding: the voice itself, a speaker embedding from the Qwen3-TTS Base model's speaker encoder (any 5-15 s of clean speech, no transcript needed). Instead of an embedding, "Speaker": "ryan" uses one of the model's built-in speakers (serena, vivian, uncle_fu, ryan, aiden, ono_anna, sohee, eric, dylan).
warning

Never ship a real person's voice without their consent. The official voices are blends of two or three volunteers from open datasets (CC0 / CC-BY), so no voice is one person's; children and the narrator are designed from a written description. ul_Voices/Voice/Voices/SOURCES.txt lists them.

Moods.ulvoicemood

json{
	"Default": "Speak naturally, in an everyday conversational tone.",
	"Narrator": "Read in a calm, steady, slightly low storytelling voice.",
	"Moods": {
		"Furious": "Shout at the top of your voice in furious anger, very loud, almost screaming.",
		"Crying": "Speak while crying and sobbing, voice trembling and breaking, tearful."
	}
}

The mood is the second argument of Game:Say(actor, mood, text), matched without case. A mood missing here uses Default. The instruction is plain English and describes the delivery, not the words: volume, pace, breath, what the voice does.

Fillers

json{
	"Fillers": {
		"Crying": { "Kind": "sob", "Chance": 0.7 },
		"Happy": { "Kind": "laugh", "Chance": 0.2 }
	}
}

In Moods.ulvoicemood. Before a line of that mood, with that chance, the speaker first makes a sound of that kind, picked among Voice/Fillers/<voice id>/<kind>_<n>.mp3 (any number of takes). The player's built-in speakers use the speaker's name as the folder (Fillers/ryan/). A voice without a file of that kind just says the line. Keep chances low on frequent moods: a giggle before every flirty line gets old fast.

ul_Voices ships 11 kinds for every voice: laugh, giggle, chuckle, huff, scoff, sigh, sob, gasp, groan, hum, nervous. They were rendered by the same model and kept only when a speech recogniser heard no words in them.

Engine.ulvoiceengine

json{
	"Exe": "Engine/ul-voice.exe",
	"Model": "Engine/qwen-talker-0.6b-customvoice-Q8_0.gguf",
	"Codec": "Engine/qwen-tokenizer-12hz-Q8_0.gguf",
	"Language": "english",
	"Narrator": "narrator",
	"PlayerMale": "ryan",
	"PlayerFemale": "vivian",
	"Temperature": 0.6,
	"SubTemperature": 0.9
}

Paths are relative to the Voice/ folder. Narrator is a voice Id (never given to a person). The last mod that ships an engine file is the one used, except that an engine marked "Fallback": true (the light mod's) is used only when no other engine is installed. Engine names the family ("qwen" when left out) and picks which voices count. Phone tunes the handset filter on remote lines:

json"Phone": { "LowHz": 350, "HighHz": 3200, "Drive": 2.5, "Noise": 0.004 }

The speech process is any program that speaks the same JSON-lines protocol on stdin/stdout: Tools/Voice/ul-voice.cpp (Qwen) and Tools/Voice/Lite/ul-voice-lite.cpp (Kokoro) in the game sources are the two references.

Settings

The player chooses in Settings → Sound: Spoken lines (Off / Dialogue / Dialogue and narration, shown only when a voice mod is installed) and the Voices volume. The console variables below override the saved choice, for tests.

Console variable Default
ul.Voice.Mode 2 0 off, 1 dialogue only, 2 dialogue and narration
ul.Voice.CacheMB 500 size of the cache of rendered lines

To play a clip you recorded yourself rather than a generated one, see Voices (Actor:speak).