voice_audio_multi/docs/models/moss_ttsd.md
Fabio c36510c21e
Some checks failed
Linux Build / Linux cpu (push) Has been cancelled
Linux Build / Linux cpu-clang (push) Has been cancelled
Linux Build / Linux vulkan (push) Has been cancelled
mac-build / macOS CPU (push) Has been cancelled
Nix Build / Nix (cpu) (push) Has been cancelled
Nix Build / Nix (vulkan) (push) Has been cancelled
windows-build / Windows CPU (push) Has been cancelled
Initial import: audio.cpp with Orange Pi documentation
2026-11-04 19:05:22 +01:00

6.8 KiB

MOSS-TTSD

Dialogue text-to-speech: a speaker-tagged conversation rendered in one take, with per-speaker zero-shot voice cloning. 8B Qwen3 backbone, 16 codebooks, 24 kHz.

MOSS-TTSD is the dialogue member of the moss_tts_delay family, which it shares with MOSS-TTS-v1.5 and MOSS-VoiceGenerator: the same backbone, heads, delay decoder, audio-tokenizer codec and prompt assembly. What is its own is how its prompt block renders and how it clones.

Family moss_ttsd
Tasks tts, clon
Codec MOSS-Audio-Tokenizer v1, first 16 of its 32 RVQ layers
Sample rate 24 kHz mono

Speaking a dialogue

The text is speaker-tagged. Tags are positional: [S1] is the first speaker, [S2] the second, and they may alternate as often as the script does.

audiocpp_cli --task tts --family moss_ttsd --model /path/to/MOSS-TTSD-GGUF \
  --backend cuda --language English \
  --text "[S1] So what did you make of it? [S2] Honestly, I was not expecting that ending." \
  --out dialogue.wav

Sampling Options

Use these with --request-option:

Option Values Default Meaning
temperature float > 0 1.5 Audio-token sampling temperature.
repetition_penalty float > 0 1.1 Audio repetition penalty; checked when token history is present.

Cloning the speakers

--request-option instruct=<text> supplies an optional instruction; legacy instruction remains accepted. --request-option text_temperature=<float> controls text-stream sampling (default 1.5); legacy moss_ttsd.text_temperature remains accepted.

Pass one reference recording per speaker, comma separated and positional, and the transcript of what those recordings say:

audiocpp_cli --task clon --family moss_ttsd --model /path/to/MOSS-TTSD-GGUF \
  --backend cuda --language English \
  --request-option voice_samples=/path/to/s1.wav,/path/to/s2.wav \
  --request-option "reference_text=[S1] This is the first voice. [S2] And this is the second." \
  --text "[S1] So what did you make of it? [S2] Honestly, I was not expecting that ending." \
  --out dialogue.wav

The number of entries is the number of speakers, and an entry with no path is a speaker that is named but not cloned:

voice_samples= speakers
s1.wav,s2.wav both cloned
s1.wav, [S1] cloned, [S2] invented
,s2.wav [S1] invented, [S2] cloned
s1.wav one speaker only — no [S2] is implied

So the trailing separator is meaningful, not a typo: s1.wav, and s1.wav are different prompts.

Why reference_text matters

This model does not clone by showing the reference and then starting fresh. It clones by continuation: the reference recording is handed to the model as audio it has already produced, and the dialogue is generated as a continuation of it. reference_text is what that audio says, and it is prepended to your text so the words and the audio line up. Omitting it leaves the model continuing a recording whose transcript it was never given, which is worth hearing before you rely on it.

Packages and what fits

package size clones on a 24 GiB card
moss_ttsd_q8_0_codec_f16 (default) 12.2 GB yes
moss_ttsd_q4_k_codec_f16 9.5 GB yes
moss_ttsd_bf16_codec_f16 18.9 GB no

⚠ bf16 cannot clone on a 24 GiB card. Cloning needs the codec encoder as well as the decoder, and its weights want a further 3.5 GB on top of the backbone; on an RTX 3090 that fails at moss.audio_tokenizer.encoder. Plain generation in bf16 is fine. Use q8_0 to clone on 24 GiB, or run bf16 on CPU.

q4_k needs its heads left alone

⚠ A plain q4_k of this model is broken, and quietly: it speaks the first turn of a dialogue and stops.

q4_k, heads quantised   3.20s  "The train leaves at four in the afternoon. Ah, back, please."
q4_k, heads quantised   2.56s  "The train leaves at 4 in the afternoon."
q4_k, heads at f16     16.48s  full script
q8_0                   12.48s  full script

lm_heads.0 is the text head, and it is what decides when the turn ends. Quantised to four bits it ends the turn early, which reads as a model that cannot hold a dialogue rather than as a bad quantisation. The published package is therefore built with the heads kept at f16, which costs 0.95 GB:

audiocpp_gguf ... --type q4_k \
  --keep-type "audio_tokenizer_weights*=f16" \
  --keep-type "model_weights/lm_heads*=f16"

Note the namespace separator is /, not .. A pattern that does not match is not an error -- it silently produces the broken package.

How well does it clone?

Not reliably, and this is the model rather than this port. Measuring median F0 per turn against two references 84 Hz apart, across three runs of the reference implementation and one of ours:

run                S1 turn1  S1 turn2  S2 turn1  S2 turn2
reference run 1      200.0     110.3     154.8     131.9
reference run 2      203.4     102.8     179.1     160.0
reference run 3      208.8     104.8     161.1     152.5
ours (seed 7)        210.5     120.6     166.7     164.4
references:          S1 = 201.7 Hz, S2 = 117.6 Hz

The first speaker's first turn matches its reference closely. By that speaker's second turn the pitch has fallen to roughly the other reference's, and the second speaker sits between the two throughout. The reference implementation does this in every run and ours reproduces the pattern at the same magnitude.

Median F0 is a crude stand-in for speaker identity and this is four runs of one configuration with short references, so treat it as "identity drifts across turns" rather than as a measured rate. Longer references may do better; the model card's own example uses them.

Separately, one take in four ended with a short spurious utterance after the script had finished. Too few takes to attribute it to anything.

Things worth knowing

  • No duration budget. The family's - Tokens: field is baked to None in this checkpoint's own template, so unlike v1.5 there is nothing to request. The max_frames option is a ceiling against a run that never stops, not a target.
  • No scene control, despite the field in the upstream message class: the reference renderer substitutes the literal "None" for it unconditionally, so no value has ever reached the model.
  • The text is not normalised, unlike v1.5's. Numbers and abbreviations are spoken as the model reads them, and the speaker tags are passed through untouched.
  • One take, not chunked. A long single-voice text can be split and rendered piece by piece; a dialogue cannot, because which speaker a fragment belongs to depends on the tags before it and each piece would restart the continuation. Very long scripts are bounded by the backbone's context rather than by chunking.