Nodes/MOSS-TTS 1.5/MOSS-TTS Concat Tokens
ComfyUI Node

MOSS-TTS Concat Tokens

Glue Two Voices Together Without Touching a Single Waveform

By eehrich·Created 4 months ago·Updated 21 days ago· 3
MOSS-TTS Concat Tokens
  • tokens_a
  • tokens_b
  • tokens_c
  • tokens_d
  • tokens
  • frames

If you're cloning a voice for a one-off line, you don't need this node. If you're narrating something long - a chapter, a scene, a batch of segments that has to sound like one speaker - you eventually hit the problem this solves.

The obvious approach is to glue WAVs together and re-encode the result as your reference. That works, but it means decode, encode and resample on every run, and any level difference between clips shows up in the joint. MOSS-TTS Concat Tokens skips all of it: it joins the codes end to end along the time axis. No decode, no re-encode, no resample. MOSS never sees a waveform at that step.

How it works

MOSS audio codes are a tensor of shape [frames, n_vq] at 12.5 frames per second. Concatenating is a torch.cat on the frame axis, and the node checks that every input has the same n_vq first - that's the model check. All inputs must come from the same model; a mix-up stops with a clear error rather than producing a chimera.

The output is tokens (the merged stream) and frames (the total). frames / 12.5 is your duration in seconds, which is the number you actually care about, because the whole point of a sliding window is keeping it inside a budget.

Two to four inputs:

  • tokens_a - required. For a sliding window this is your fixed base-voice anchor.
  • tokens_b - required. Appended after tokens_a; typically the most recent segment.
  • tokens_c, tokens_d - optional, appended in order.

That's it. There are no settings, no mode switches, no trimming - which is exactly why it can bite you (see below).

The sliding-window pattern

This is the use case the node was written for, and it's the one that keeps long-form narration coherent:

[Load Tokens (base voice)] -> tokens_a ┐
                                       ├-> [Concat Tokens] -> tokens
[prev segment's tokens]   -> tokens_b ┘         │
                                                v
                              [Voice Clone].reference_tokens
                              (or [Voice Continue].prev_tokens)

Keep the base anchor in tokens_a so the voice identity never drifts, and append the most recent segment(s) so the model knows where it actually is in the script. Because generations emit tokens too, the "most recent segment" link never leaves code space - the loop finishes without the codec touching anything.

The trap, and it's a real one

Concat Tokens sees codes and nothing else. It cannot check your transcript.

If you drop frames from the window, drop the words from previous_text to match. A sliding window that quietly trims the oldest sentences while previous_text still holds the whole scene is the classic way to get gibberish - not a slightly-off voice, unusable audio. MOSS aligns the spoken prefix against its transcript, and a pair that doesn't describe the same speech is meaningless to it.

And keep the window at ~10 seconds minimum - about 125 frames. Below that MOSS doesn't have enough acoustic evidence to lock onto the voice. Since frames is right there on the output, wiring it into a check (or just eyeballing it during tuning) is the cheap guard.

One more thing nobody expects: Encode Tokens normalises each clip on its own, so two concatenated token streams can be a hair off in level compared to a single WAV made of both clips. Both work fine. If you're chasing a small loudness step at a joint, that's where it came from.

Installing it

ComfyUI-Manager: search MOSS-TTS 1.5 (by enrico) - other packs use similar names. Or by hand:

cd ComfyUI/custom_nodes
git clone https://github.com/eehrich/ComfyUI-MOSS-TTS-1.5.git

Restart ComfyUI. The pack declares no pip dependencies at all (its pyproject.toml is explicit about that) - it uses the torch, torchaudio and transformers ComfyUI already ships. Model weights are the real download: ~9 GB plus ~8.5 GB of audio tokenizer for the default model, unless they're already sitting in ComfyUI/models/moss_tts.

Troubleshooting

  • Gibberish after a window trim - the transcript wasn't trimmed with it. The Decode Tokens node is the fastest way to hear what the window actually contains; a reference that sounds wrong there will clone wrong.
  • Error about n_vq on a concat - you mixed codes from the default model and the 8B. They are not interchangeable; re-encode the odd one out.
  • The window keeps growing run after run - nothing here enforces a limit, so a naive chain that always appends the newest segment ends up carrying a minute of audio and several GB of VRAM. Keep the anchor plus the last segment or two, not the whole history.
  • non-default argument 'sampling_rate' at model load - transformers 5.4.0/5.5.0. python -m pip install -U "transformers>=5.5.1".

Honest verdict: a single-voice, single-segment job doesn't need this. A multi-segment narration or an API pipeline that keeps re-cloning one voice over a growing script does, and this is the node that makes those two things stop fighting each other.

CategoryMOSS TTS 1.5

Inputs (4)

NameTypeDefaultDescription
tokens_aMOSS_TOKENSFirst stream. For a sliding window this is the base voice anchor.
tokens_bMOSS_TOKENSSecond stream, appended after tokens_a (e.g. the most recent segment).
tokens_coptMOSS_TOKENSOptional third stream, appended after tokens_b.
tokens_doptMOSS_TOKENSOptional fourth stream, appended after tokens_c.

Outputs (2)

NameTypeDescription
tokensMOSS_TOKENSConcatenated codes, shape [sum(frames), n_vq].
framesINTTotal frame count. Divide by 12.5 for seconds -- handy to keep a sliding-window reference inside a duration budget.