OuteTTS validation

July 31, 2026 ยท View on GitHub

This procedure exercises OuteTTS 1.0 1B in one long-lived audio.cpp session. It covers normal TTS, framework long-form text chunking, voice cloning, repeated reference-profile cache hits, cached-step graph reuse, stage timings, and default-versus-mem_saver memory behavior.

Model setup

Install the safetensors model, IBM DAC, and Qwen3 Forced Aligner resources:

python tools/model_manager_deprecated.py install outetts_1_0_1b --models-dir models

The model-manager package id is outetts_1_0_1b. It creates these model roots:

  • models/Llama-OuteTTS-1.0-1B
  • models/DAC.speech.v1.0
  • models/Qwen3-ForcedAligner-0.6B

The standalone GGUF command is documented in TTS. The packed file used below contains the OuteTTS language model, IBM DAC, Qwen aligner, tokenizers, configuration, and package specification.

Python reference setup

The parity run used the official OuteTTS repository at commit f5eac6e70d792844c6a6959d900a47af2c061a5b (outetts 0.4.4), Python 3.10, PyTorch 2.5.1+cu121, and Transformers 4.52.3:

git clone https://github.com/edwko/OuteTTS build\reference\OuteTTS
git -C build\reference\OuteTTS checkout f5eac6e70d792844c6a6959d900a47af2c061a5b
python -m venv --system-site-packages build\reference\venv
build\reference\venv\Scripts\python.exe -m pip install -e build\reference\OuteTTS --no-deps
build\reference\venv\Scripts\python.exe -m pip install transformers==4.52.3 llama-cpp-python==0.3.9 polars natsort mecab-python3 unidic-lite uroman openai-whisper ftfy pyloudnorm

The reference loaded models/Llama-OuteTTS-1.0-1B through outetts.Backend.HF with FP32 CUDA weights and loaded the official IBM DAC checkpoint from models/DAC.speech.v1.0/weights_24khz_1.5kbps_v1.0.pth. Both implementations used temperature 0.4, repetition penalty 1.1 over the latest 64 tokens, top-k 40, top-p 0.9, min-p 0.05, the request-file seeds, and the same maximum number of new tokens.

Temperature zero was deliberately not used. OuteTTS 1.0's official generation_config.json, README example, and Python API all select sampled generation with temperature 0.4; changing to greedy generation would test a different inference contract. OuteTTS has no diffusion or latent-noise input. Its only request-time randomness is language-model token sampling.

For a controlled clone comparison, Qwen word timings were generated once and passed to the official Python AudioProcessor.create_speaker_from_dict path:

build\windows-cuda-release\bin\audiocpp_cli.exe `
  --task align --family qwen3_forced_aligner `
  --model ..\models\Qwen3-ForcedAligner-0.6B_Q8\Qwen3-ForcedAligner-0.6B_Q8.gguf `
  --backend cuda --audio assets\resources\b.wav `
  --text "Some call me nature. Others call me Mother Nature. I've been here for over 4.5 billion years. 22,500 times longer than you." `
  --language en --words-out build\reference\b_words.json

Python still performed the official DAC encoding and feature extraction. This keeps different alignment algorithms from contaminating the TTS comparison. The audio.cpp cold-clone timing includes its Qwen alignment, while the Python number starts from the supplied word timings.

Run the committed official-reference driver after creating the alignment JSON:

build\reference\venv\Scripts\python.exe `
  tools\community_models\outetts_reference.py `
  --model ..\models\Llama-OuteTTS-1.0-1B `
  --dac ..\models\DAC.speech.v1.0\weights_24khz_1.5kbps_v1.0.pth `
  --alignment-json build\reference\b_words.json `
  --request-file tests\outetts\warm_bench_requests.json `
  --device cuda --dtype fp32 `
  --out-dir build\reference\official_script_fp32

The driver resets both torch.manual_seed and torch.cuda.manual_seed_all immediately before every model.generate call, after all prompt, alignment, and DAC-profile work. It writes native 24 kHz WAVs plus boundaries.json, memory.json, stdout.log, and command.json.

Exact build commands

The Windows validation used the project build script to establish the compiler and backend configuration, then explicitly enabled the warm-benchmark targets.

Windows CPU:

.\scripts\build_windows.ps1 -Preset windows-cpu-release -ConfigureOnly
cmake -S . -B build\windows-cpu-release -DENGINE_BUILD_WARMBENCH=ON
cmake --build build\windows-cpu-release --parallel 4 --target audiocpp_cli audiocpp_server outetts_warm_bench

Windows CUDA 12.4:

$env:CUDA_PATH = "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.4"
.\scripts\build_windows.ps1 -Preset windows-cuda-release -ConfigureOnly
cmake -S . -B build\windows-cuda-release -DENGINE_BUILD_WARMBENCH=ON
cmake --build build\windows-cuda-release --parallel 4 --target audiocpp_cli audiocpp_server outetts_warm_bench

Exact standalone-GGUF path tests

The tested GGUF was deliberately outside the PR worktree and its directory contained only one file:

..\models\Llama-OuteTTS-1.0-1B_Q8\Llama-OuteTTS-1.0-1B_Q8.gguf

Normal TTS:

build\windows-cuda-release\bin\audiocpp_cli.exe `
  --task tts --family outetts `
  --model ..\models\Llama-OuteTTS-1.0-1B_Q8\Llama-OuteTTS-1.0-1B_Q8.gguf `
  --backend cuda `
  --text "This is the standalone GGUF path test." `
  --max-tokens 256 --request-option seed=1234 `
  --out ..\outputs\outetts_path_test_tts.wav

Voice cloning using the aligner and DAC embedded in the same GGUF:

build\windows-cuda-release\bin\audiocpp_cli.exe `
  --task clon --family outetts `
  --model ..\models\Llama-OuteTTS-1.0-1B_Q8\Llama-OuteTTS-1.0-1B_Q8.gguf `
  --backend cuda `
  --voice-ref assets\resources\b.wav `
  --reference-text "Some call me nature. Others call me Mother Nature. I've been here for over 4.5 billion years. 22,500 times longer than you." `
  --request-option reference_language=en `
  --text "This is the standalone GGUF cloning path test." `
  --max-tokens 256 --request-option seed=42 `
  --out ..\outputs\outetts_path_test_clone.wav

Both commands loaded successfully without an external package spec, tokenizer, DAC directory, aligner directory, or other sidecar. The generated 24 kHz mono WAVs passed ffmpeg decoding:

ArtifactDurationBytesSHA-256
..\outputs\outetts_path_test_tts.wav1.399667s6722892DA5D8438D4E6A79FBDDBD8EDA77D1AFD2A9B47F61085ECD3AD45C913102904
..\outputs\outetts_path_test_clone.wav1.453000s69788E5B46E62C2DB89A654F7EC21A7BE31F575D6F667F8578224386E7C552492B05E

Exact long-lived-session runs

CUDA default:

build\windows-cuda-release\bin\outetts_warm_bench.exe `
  --model ..\models\Llama-OuteTTS-1.0-1B_Q8\Llama-OuteTTS-1.0-1B_Q8.gguf `
  --backend cuda --threads 8 `
  --request-file tests\outetts\warm_bench_requests.json `
  --hold-seconds 5 `
  --audio-out-dir ..\outputs\outetts_review_cuda_default `
  --log-file build\logs\warmbench\outetts-cuda-default.log

Expected trace evidence:

  • outetts.text_chunk_count is greater than one for tts_longform.
  • outetts.llama.step.graph_reused=1 appears after the first compatible generation request or chunk.
  • the second identical reference reports outetts.reference_cache.hit=1.
  • tts_cold and tts_repeat are byte-identical, as are clone_cold and clone_repeat; this verifies that warm graph/profile reuse does not change deterministic output.
  • outetts.aligner.runtime_reused=1 is observable for uncached references while the default session retains the aligner.
  • only one active OuteTTS Llama runtime is retained; switching between native TTS weights and the CUDA F32 cloning fallback replaces the previous runtime.

Run the same request sequence in a fresh process with memory saver enabled:

build\windows-cuda-release\bin\outetts_warm_bench.exe `
  --model ..\models\Llama-OuteTTS-1.0-1B_Q8\Llama-OuteTTS-1.0-1B_Q8.gguf `
  --backend cuda --threads 8 `
  --request-file tests\outetts\warm_bench_requests.json `
  --session-option outetts.mem_saver=true `
  --hold-seconds 5 `
  --audio-out-dir ..\outputs\outetts_review_cuda_mem_saver `
  --log-file build\logs\warmbench\outetts-cuda-mem_saver.log

CPU default:

build\windows-cpu-release\bin\outetts_warm_bench.exe `
  --model ..\models\Llama-OuteTTS-1.0-1B_Q8\Llama-OuteTTS-1.0-1B_Q8.gguf `
  --backend cpu --threads 8 `
  --request-file tests\outetts\warm_bench_requests.json `
  --hold-seconds 5 `
  --audio-out-dir ..\outputs\outetts_review_cpu_default `
  --log-file build\logs\warmbench\outetts-cpu-default-final.log

Safetensors CUDA parity run (DAC is resolved by the package spec; the aligner path is supplied because it is not embedded in the safetensors directory):

build\windows-cuda-release\bin\outetts_warm_bench.exe `
  --model ..\models\Llama-OuteTTS-1.0-1B `
  --backend cuda --threads 8 `
  --request-file tests\outetts\warm_bench_requests.json `
  --session-option outetts.weight_type=f32 `
  --session-option outetts.aligner_path=..\models\Qwen3-ForcedAligner-0.6B `
  --audio-out-dir build\reference\cpp_f32_final_explicit `
  --log-file build\reference\cpp_f32_final_explicit.log

The memory-saver trace reports a positive outetts.llama.step.released_cache_capacity after generation and outetts.aligner.runtime_released=1 after an uncached reference.

Sample total-device VRAM every 250 ms while each fresh benchmark runs. Ensure that no unrelated CUDA workload is active:

nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits -lms 250

Compare peak and final resident VRAM, request wall time, output duration, and RTF between the two runs. Keep model, backend, device, seed, requests, and quantization identical.

Generated artifacts

Each benchmark directory contains:

tts_cold_1.wav
tts_repeat_1.wav
tts_longform_1.wav
clone_cold_1.wav
clone_repeat_1.wav

The exact generated directories and logs were:

  • ..\outputs\outetts_review_cuda_default
  • ..\outputs\outetts_review_cuda_mem_saver
  • ..\outputs\outetts_review_cpu_default
  • build\logs\warmbench\outetts-cuda-default.log
  • build\logs\warmbench\outetts-cuda-mem_saver.log
  • build\logs\warmbench\outetts-cpu-default-final.log
  • build\logs\warmbench\outetts-ab-torch-sampler\default-{1,2,3}-audio
  • build\logs\warmbench\outetts-ab-torch-sampler\mem_saver-{1,2,3}-audio
  • build\reference\cpp_f32_final_explicit
  • build\reference\cpp_q8_torch_sampler
  • build\reference\official_script_fp32\outputs
  • build\reference\official_script_fp32\boundaries.json

These validation artifacts are reproducible local outputs and are not committed to the repository.

Measured validation

The committed five-request sequence was measured with the packed Q8 GGUF on an NVIDIA GeForce RTX 3090 (CUDA 12.4). Each CUDA mode was run in three fresh processes in alternating order. Values below are mean +/- sample standard deviation. The Python HF and audio.cpp safetensors columns are single loaded- session runs; model load is excluded from every per-request measurement.

RequestPython HF FP32 wall / RTFC++ FP32 wall / RTFC++ Q8 wall / RTF
tts_cold4599.12 ms / 3.4855366.24 ms / 4.0663211.84 +/- 17.30 ms / 2.434
tts_repeat3777.69 ms / 2.8633864.98 ms / 2.9293030.79 +/- 18.60 ms / 2.297
tts_longform15129.91 ms / 2.69015547.80 ms / 2.76413015.83 +/- 171.29 ms / 2.342
clone_cold5762.51 ms / 3.7925988.01 ms / 3.9406432.68 +/- 65.11 ms / 4.233
clone_repeat5522.67 ms / 3.6344231.19 ms / 2.7844220.23 +/- 44.07 ms / 2.777

These are per-request timings from a single loaded session; model loading is excluded. Python and C++ FP32 generated 1.31967, 1.31967, 5.62533, 1.51967, and 1.51967 seconds respectively. Q8 generated 5.55867 seconds for the long-form request and matched the other four durations.

The memory-saver A/B is also reported per request rather than inferred from a single aggregate run:

RequestDefault CUDA Q8Memory saver CUDA Q8Wall deltaDefault RTFMemory-saver RTF
tts_cold3211.84 +/- 17.30 ms3223.24 +/- 12.89 ms+0.35%2.4342.443
tts_repeat3030.79 +/- 18.60 ms3044.58 +/- 18.76 ms+0.45%2.2972.307
tts_longform13015.83 +/- 171.29 ms13070.17 +/- 105.32 ms+0.42%2.3422.351
clone_cold6432.68 +/- 65.11 ms6467.00 +/- 46.38 ms+0.53%4.2334.256
clone_repeat4220.23 +/- 44.07 ms4238.27 +/- 17.07 ms+0.43%2.7772.789

Memory saver was slightly slower on every request in this controlled repeat: 0.35% to 0.53%, and 0.44% over the mean five-request wall total. These small differences are near normal run-to-run variance, so there is no evidence that memory saver is a speed optimization. It remains useful because the independently sampled peak VRAM fell from 17653 MiB to 5780 MiB (67.3%) with the same 294 MiB post-session resident VRAM. It is therefore a memory-versus-speed option, not a speed optimization.

The Windows CPU Q8 run is likewise reported per request:

RequestWallAudioRTF
tts_cold10517.2 ms1.31967s7.970
tts_repeat10349.9 ms1.31967s7.843
tts_longform41604.0 ms5.67867s7.326
clone_cold22849.3 ms1.58633s14.404
clone_repeat18205.6 ms1.58633s11.477

All generated WAV files passed an ffmpeg decode check. The traces confirmed four framework chunks for the long-form request, compatible step-graph reuse in default mode, explicit graph release in memory-saver mode, and a reference- profile cache hit on the repeated clone. In the original instrumented default CUDA run, cold reference preparation took about 890 ms (109 ms alignment, 139 ms DAC encoding, and 214 ms profile construction), while the repeated cached reference took 0.29 ms.

Path and parity results

  • The standalone path test loaded the one-file GGUF from outside the worktree; no adjacent sidecars or model_specs directory were present.
  • Within CUDA, cold and repeated TTS were byte-identical with SHA-256 9D1AF0D25212129B3C26880F7C928CFFA813A4B9EF22F12F390A4C372D51D406.
  • Within CUDA, cold and repeated cloning were byte-identical with SHA-256 EB7E6B5A1330F845F45D3DA21C8AC28490572F95218A6F0D3DA2A83CB797EFE6.
  • The CUDA default and memory-saver outputs had the same hashes.
  • Within CPU, cold and repeated TTS were byte-identical with SHA-256 A315C0AD425C99910597FCC4BAB9D80CB5F4C893176EBD9099BA6C6AA7B66BFD.
  • Within CPU, cold and repeated cloning were byte-identical with SHA-256 1A2009C0D512773A4B5082BF37EDEDD6A52A37CC6D556772C62EA26550A83B4D.
  • CPU and CUDA WAV bytes are not identical because backend floating-point execution differs; deterministic reuse parity was therefore checked within each backend rather than asserted across backends.

Python parity uses the repository's tools/audiocpp_cli/compare_audiocpp_cli_path_results.py implementation: PCM WAV cosine plus an 80-band log1p mel-spectrogram cosine (n_fft=1024, hop=256). Both sides write the DAC-native 24 kHz signal directly. Raw WAV cosine is highly phase-sensitive; log-mel cosine is the more informative value when the DAC encoders are numerically close but not bit-identical.

The CUDA sampler now uses the framework's PyTorch-compatible exponential-race random stream, equivalent to the torch.multinomial path used by Transformers. Temporary trace-level instrumentation compared prompt strings, prompt token IDs, generated token IDs, codec codes, and WAV frame counts at component boundaries. It was removed after validation to avoid normal-log spam. The permanent Python driver records the Python side in boundaries.json.

Boundary results under the controlled FP32 run:

RequestChunksPython/C++ prompt IDsGenerated IDs per chunkPython/C++ frames
tts_cold122 / 2225631672 / 31672
tts_repeat122 / 2225631672 / 31672
tts_longform420,22,14,21 / same256 each135008 / 135008
clone_cold12367 / 236725636472 / 36472
clone_repeat12367 / 236725636472 / 36472

The sampled prompt/generated boundary values for all six ordinary-TTS chunks had zero mismatches. Clone prompt construction also matches the official word labels and token count. The C++ DAC reference encoder still differs slightly from Python: among 1046 reference-profile frames, C1 differs on 48 and C2 on 118. That numerical boundary explains why cloning is not waveform-identical.

Primary native/FP32 parity, measured only after the controlled random stream and frame counts matched:

RequestWAV cosineLog-mel cosineC++ / Python frames
tts_cold0.9998122470.99999748131672 / 31672
tts_repeat0.9998122380.99999748331672 / 31672
tts_longform0.9973245300.999294328135008 / 135008
clone_cold-0.0077530640.88940384536472 / 36472
clone_repeat-0.0077530640.88940384536472 / 36472

Only after that path was clean was the packed Q8 model compared with the same FP32 Python reference. Quantization is expected to perturb autoregressive sampling, so this is a secondary characterization rather than the correctness gate:

RequestWAV cosineLog-mel cosineC++ Q8 / Python frames
tts_cold-0.0163261710.69679579231672 / 31672
tts_repeat-0.0163265910.69679275131672 / 31672
tts_longform0.5779275750.920741352133408 / 135008
clone_cold-0.0339907750.88271585536472 / 36472
clone_repeat-0.0339907750.88271585536472 / 36472

Backend coverage

BackendCoverage
Windows CPUCLI/server/warm-benchmark build; normal TTS, long-form TTS, cloning, cache reuse, graph reuse, WAV decode, timing, and RSS run
Windows CUDA 12.4CLI/server/warm-benchmark build; normal TTS, long-form TTS, cloning, standalone-path loading, cache/graph reuse, three-run default/memory-saver A/B, Python HF parity, WAV decode, timing, RTF, and VRAM run
Linux CPUGitHub Actions compile check passed for CLI, server, and GGUF converter; no model runtime measurement claimed
Linux VulkanGitHub Actions compile check passed for CLI, server, and GGUF converter; no model runtime measurement claimed
macOS CPUGitHub Actions compile check passed for CLI, server, and GGUF converter; no model runtime measurement claimed
MetalNot enabled or runtime-tested in this validation

Known limitations

  • OuteTTS is offline-only in this implementation.
  • Voice cloning requires an accurate transcript of the reference WAV and rejects references longer than 20 seconds.
  • Quantized CUDA cloning expands the OuteTTS language-model weights to F32 for generation correctness. Default mode can therefore have a high transient peak while the aligner is retained; outetts.mem_saver=true releases the aligner and cached-step graph between phases.
  • Long-form output concatenates independently generated chunks. max_tokens applies to each chunk; the runtime splits text further to fit an explicit cap and retries a chunk that reaches the cap without EOS/audio-end as smaller chunks rather than returning silently truncated speech. It reports an error only when the remaining text cannot be split any further.
  • CPU and CUDA outputs are deterministic within each tested backend but are not expected to be byte-identical across backends.
  • The controlled CUDA sampling stream is PyTorch-compatible. CPU sampling is deterministic for a fixed seed but still uses the C++ standard-library distribution and is not claimed to reproduce PyTorch token-for-token.
  • Quantized weights can change sampled tokens and output length even when the seed and sampler implementation match, as shown by the Q8 long-form frame drift above.