Training a Local GLaDOS TTS Voice #

GLaDOS TTS build pipeline visualization

Build kit repo: JoeHelbing/glados-tts-build-kit

TLDR #

  • Pulled the GLaDOS voice lines out of the Portal and Portal 2 game files
  • Transcribed them with Cohere Transcribe through CohereX
  • Checked those transcripts against Portal Wiki ground truth
  • Hand-reviewed the messy clips in a local web UI
  • Used the cleaned set to train a local TTS voice model
  • Credit: I got the game-file idea from systemofapwne/piper-de-glados

Sample: "The quick brown fox jumps over the lazy dog. The fox will be rewarded with cake. The dog will be recycled."

Motivation #

I wanted to train a local GLaDOS TTS voice partly for the voice-training experience, but also because having GLaDOS as the voice of a personal assistant would be entertaining.

The point was always to do an actual fine tune on an open source model. Not a voice clone, not a LoRA, not a QLoRA, and not a SaaS ElevenLabs fine tune.

Here is the checkpoint comparison from the second OmniVoice run:

OmniVoice v2 checkpoint-1500

early checkpoint, still close to the first useful run

OmniVoice v2 checkpoint-3500

best eval loss on the second OmniVoice run

OmniVoice v2 checkpoint-5000

late endpoint, useful mostly as an overtraining check

Checkpoint-3500, seed 42, speed 1.15

seed and speed smoke test

The Wrong First Step #

I started with a 29 minute and 50 second YouTube compilation of GLaDOS voice lines. I assumed the combination of "clean" voice data and timestamped closed captioning would work well.

Downloading and examining it showed obvious problems: music, singing, transitions, sound effects, and captions that were not good enough for a training manifest.

The Data Was Already On Disk #

From systemofapwne/piper-de-glados, I got the idea of using the Portal and Portal 2 voice assets in VPK archives, Valve's package files for Source Engine assets. The useful paths were things like sound/vo/glados/*, sound/vo/aperture_ai/*, and sound/vo/escape/*.

Between Portal and Portal 2, there were 1,400 candidate files, about 110 minutes of audio before filtering. Some of the files had .wav extensions while containing MP3-encoded audio, so the corpus needed transcoding to 24 kHz PCM mono before training.

Source directory Candidate clips
glados 1,040
aperture_ai 344
escape 16
Total 1,400

Transcription #

With clean audio pulled directly from the game files, I used Cohere Transcribe through CohereX to produce the textual representations. The full 1,400 clip pass took about 10 minutes on the RTX 3090.

Ground Truth #

I remembered that Portal 2 had subtitles when characters were speaking, so I checked whether anyone had already pulled and posted those lines. The Portal Wiki had four voice-line pages covering Portal, Portal 2, Cooperative Testing Initiative, and other lines.

I scraped those pages for 1,814 unique wiki filenames, and 1,287 of the 1,400 extracted game clips matched.

Reconciliation metric Value
Extracted game clips 1,400
Unique wiki filenames 1,814
Extracted clips matched to wiki 1,287
Mean Cohere-vs-wiki WER 8.47%

Reconcile And Filter #

The reconciliation pass sorted clips into buckets: exact matches, minor diffs, major diffs, Cohere-only rows, inferred sound effects, inline annotations, and annotation-only lines.

A lot of "major" differences were canonicalization. GLaDOS saying Nonononono as transcribed by CohereX might appear as "No, no, no..." on the wiki, and the distance metric treats that as a big disagreement. These were manually reviewed. Obvious SFX filenames were dropped, inline [bzzt], [cough], and [garbled] clips were dropped, and anything suspicious enough got pushed into the manual review pile.

Bucket Included Excluded
annotation_only 0 13
exact_match 1,022 0
inferred_sfx 4 42
inline_annotation 5 21
major_diff 126 6
minor_diff 94 0
only_in_cohere 8 59

Human Review UI #

I had Codex create a local review page to quickly check the issue clips at 127.0.0.1. The page embedded each clip's audio, showed the proposed transcript, let me edit the text and include/exclude decision, and wrote the results back to the dataset.

That pass covered 284 flagged clips. Seventy-four clips were changed back to "include," and five transcripts were wrong in both CohereX and the wiki, so I manually edited them. The final manifest landed around 101 minutes, which by my reading of TTS training notes was sufficient for a narrow single-character voice fine tune.

Model Timeline #

Stage Model Result
Qwen3-TTS v1 full fine tune completed at 5.5 scalar loss, but generated unusable audio
Qwen3-TTS v2 parameter changes loss improved to about 2.2, but audio quality did not improve
OmniVoice v1 learning_rate=1e-5 first genuinely useful voice; checkpoint-1500 was already better by ear
OmniVoice v2 learning_rate=5e-6 best eval scalar at checkpoint-3500, with subtle differences after checkpoint-1500

Qwen3-TTS Failure Notes #

I initially tried Qwen3-TTS. The first run completed at a 5.5 scalar loss but produced garbled nonsense in multiple overlapping voices. The second Qwen3-TTS run changed the training parameters and got the loss down to about 2.2, but the audio did not get better. The scalar loss improved, but the actual output was still garbage.

OmniVoice Pivot #

I checked Hugging Face for the most-trending TTS model and looked at the OmniVoice GitHub repo. It had guidance on fine tuning and a fine-tuning shell script, so I switched models.

The first OmniVoice run used learning_rate=1e-5, evaluated every 500 steps, and split the data into 1,134 train clips and 125 test clips. It visibly overfit by eval loss, but checkpoint-1500 was already qualitatively better than the Qwen attempts. Not perfect, but recognizably in the right family.

Run LR Steps Outcome
OmniVoice v1 1e-5 ~2,650 checkpoint-1500 was the first useful voice
OmniVoice v2 5e-6 5,000 checkpoint-3500 had the best eval scalar, though later checkpoints were close by ear

OmniVoice V2 #

For OmniVoice v2 I lowered the learning rate to 5e-6, kept warmup_ratio=0.03, saved and evaluated every 500 steps, and set keep_last_n_checkpoints=-1 so I would not accidentally throw away checkpoints.

The best eval scalar was checkpoint-3500 at eval/loss=3.7785. I tested each checkpoint in an XY Plot to see which one had the best actual audio generation. Honestly, the differences after checkpoint 1500 were subtle and they were all quite good. I rendered a fixed prompt table across checkpoints 1500, 3000, 3500, 4000, and 5000, and also supported ad-hoc generation.

I tested seed and speed controls too, but those did not seem to affect quality much.

Step Eval loss
500 4.0669
1,000 3.9940
1,500 3.8367
2,000 3.9973
2,500 3.9479
3,000 3.9470
3,500 3.7785
4,000 3.9774
4,500 3.9788
5,000 4.2239

Repo #

The build kit repo is here: JoeHelbing/glados-tts-build-kit.

GLaDOS audio belongs to Valve Corporation. The pipeline is publishable; trained checkpoints built from proprietary audio are for personal use only.

Credits / Prior Art #

  • systemofapwne/piper-de-glados for the key idea of using Portal and Portal 2 game files rather than YouTube compilations.
  • Portal Wiki contributors for the voice-line pages used as transcript ground truth.
  • Valve and Ellen McLain for the original character and performance.