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

162 lines
6.8 KiB
Markdown

# 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](moss_tts_v15.md) and [MOSS-VoiceGenerator](moss_voicegen.md):
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.
```bash
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:
```bash
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:
```bash
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.