Model reference · open weights

moss-tts-local-transformer-voice-acting-sft3

Audio laion Text→speech 1 build Open weights 722 dl/mo

moss-tts-local-transformer-voice-acting-sft3 is an open-weight audio or speech model from laion. moss-tts-local-transformer-4.55b-voice-acting-v2-sft3 (BF16) weighs 8.3 GB; the smallest configuration that runs it is RTX 3060 12 GB.

For self-hosting, compare this build's requirements below with the GPU you plan to rent.

  • moss-tts-local-transformer-voice-acting-sft3 is a text-to-speech model by laion with 4.1B parameters that supports English and German.
  • It is a supervised fine-tune designed for voice acting, offering explicit control over timing, pauses, and vocal bursts through inline delivery directions.
  • The model is released under the cc-by-4.0 licence.

Summary of the laion/moss-tts-local-transformer-4.55b-voice-acting-v2-sft3 model card, 2026-10-01

What it is

Released bylaion
Released2026-08-26
Parameters4.1B
VRAM8.3 GB for the weights

What it runs on

Memory and cards for moss-tts-local-transformer-4.55b-voice-acting-v2-sft3 (BF16)

8.3 GBweights, file size
1.6 GBruntime overhead
CardOne streamMemory
RTX 3060 12 GBfits11.6 GB
RTX 4060 Ti 16 GBfits15.4 GB
RTX 3090 24 GBfits23.4 GB
RTX 4090 24 GBfits23.4 GB
RTX 5090 32 GBfits31.0 GB
L40S 48 GBfits44.0 GB
A100 80 GBfits78.2 GB
H100 80 GBfits78.1 GB
RTX PRO 6000 Blackwell 96 GBfits93.8 GB
DGX Spark (GB10) 128 GB unifiedfits107 GB
H200 141 GBfits138 GB
B200 180 GBfits176 GB

From the model card

What laion says about moss-tts-local-transformer-voice-acting-sft3

Read the model card

A supervised fine-tune of laion/moss-tts-local-transformer-4.55b-voice-acting-v2 that adds explicit control over timing — per-sentence durations, pauses, and vocal bursts with their lengths — and inline delivery directions that say how to perform each line.

4.13 B trainable parameters: a ~4 B semantic transformer (36 layers), a ~550 M local "talker" transformer, and 12 audio LM heads over a 12-codebook audio tokenizer at 12.5 frames per second (one frame = 80 ms).

What this round changed, measured

Trained on 398,282 rows selected as the strongest examples of each of 40 emotions and each VoiceNet dimension, 2 epochs, 712 steps on 32 nodes. Evaluated by generating 320 clips and scoring them, not by validation loss — this project has repeatedly seen training metrics point the wrong way.

round 2round 3
word error rate on direction-carrying prompts0.4470.099
duration error, median0.100 s0.080 s
clips within 0.5 s of the requested length92.8 %100 %
vocal-burst hit rate0.5160.666

Round 2 had accidentally dropped the delivery directions its predecessor was trained with, and a model that has never seen a direction falls apart when given one — word error rate 0.48–0.51 on such prompts, for every round-2 model. Round 3 trained them back in. Timing control is solved.

Emotional intensity is not. Asked for percentile 0.90–0.98 of a named emotion, this model reaches about 0.35. Several objectives were tried against that — GRPO with a group-relative reward, DPO with contrastive pairs, DPO with symmetric instruction-conditioned pairs — and none moved it. The emotion adapters below are the only thing that has, and even they are not selective enough to merge in blindly. This is documented honestly in the technical report.

Inference

import torch, torchaudio
from transformers import AutoProcessor, AutoModel

BASE = "laion/moss-tts-local-transformer-4.55b-voice-acting-v2-sft3"

proc = AutoProcessor.from_pretrained(BASE, trust_remote_code=True)
model = AutoModel.from_pretrained(BASE, trust_remote_code=True,
                                  dtype=torch.bfloat16, attn_implementation="sdpa").cuda().eval()

prompt = open("prompt.txt").read()          # the  block, see "How to prompt" below
um = {"role": "user", "content": prompt, "audio_codes_list": []}
b = proc([[um]], mode="generation")

with torch.no_grad():
    out = model.generate(input_ids=b["input_ids"].cuda(),
                         attention_mask=b["attention_mask"].cuda(),
                         max_new_frames=340, do_sample=True,
                         audio_temperature=1.0, audio_top_p=0.95, audio_top_k=50,
                         audio_repetition_penalty=1.0)

# codes -> waveform. Use the processor's own decoder: calling the audio tokenizer directly, or
# reshaping its output, yields a two-channel result that flattens into audio at HALF SPEED and
# still sounds like speech. This project lost a whole corpus to that once.
wav = proc.decode_audio_codes([out_codes], return_stereo=False)[0].reshape(-1).float().cpu()
torchaudio.save("out.flac", wav[None], int(proc.model_config.sampling_rate), format="flac")

Loading adapters

from peft import PeftModel

# one adapter
model = PeftModel.from_pretrained(model, "laion/moss-va-sft3-dpo-lora")

# several, each with its own weight -- the usual case: identity from a voice adapter,
# affect from an emotion adapter, general quality from the DPO adapter.
#
# NOTE: `add_weighted_adapter(..., combination_type="linear")` does NOT work here. It raises
# `ValueError: All adapters must have the same r value`, because the DPO adapter is rank 64 and
# the voice / emotion adapters are rank 16. Activate them together instead and scale each one.
model = PeftModel.from_pretrained(model, "", adapter_name="dpo")
model.load_adapter("", adapter_name="voice")
model.load_adapter("", adapter_name="emo")

names = ["dpo", "voice", "emo"]
weights = {"dpo": 1.0, "voice": 1.0, "emo": 1.5}     # 1.5 for emotion is the measured optimum
model.base_model.set_adapter(names)                  # the TUNER takes a list; PeftModel does not
model.active_adapter = names[0]                      # must stay a str or generate() indexes a list

for mod in model.modules():
    sc = getattr(mod, "scaling", None)
    if isinstance(sc, dict):
        if not hasattr(mod, "_base_scaling"):
            mod._base_scaling = dict(sc)
        for k in sc:
            if k in weights:
                sc[k] = mod._base_scaling[k] * weights[k]

Scaling an adapter without re-merging

A LoRA layer computes h + scaling · B(A(x)), so multiplying the stored scaling is the merge weight — exact and reversible:

def set_lora_scale(model, w):
    for mod in model.modules():
        sc = getattr(mod, "scaling", None)
        if isinstance(sc, dict):
            if not hasattr(mod, "_base_scaling"):
                mod._base_scaling = dict(sc)
            for k in sc:
                sc[k] = mod._base_scaling[k] * w

How to prompt this model

Every request is one `` block. The fields are fixed — none may be added or removed:

- Reference(s):
{None | Speaker:  | }
- Instruction:
{GENERAL: ... and/or SCRIPT: ...}
- Tokens:
{target length in audio frames}
- Quality:
None
- Sound Event:
None
- Ambient Sound:
None
- Language:
{English | German}
- Text:
{the same script as under SCRIPT:, character for character}
FieldWhat goes in it
Reference(s)`` when a reference recording of the target voice is attached, Speaker: when only a voice name is known, otherwise None.
InstructionA GENERAL: line, a SCRIPT: block, or both.
TokensTarget length in audio frames. The tokenizer runs at 12.5 frames per second, so 12.8 s = 160 frames. This is the length budget and the numbe

Quoted from the model card on Hugging Face. The full card is behind the Hugging Face link above.

© 2026 AxForge · EU-hosted AI infrastructure Pricing Docs Trust Privacy Terms