How to use from the
Use from the
VoxCPM library
import soundfile as sf
from voxcpm import VoxCPM

model = VoxCPM.from_pretrained("Rabe3/VoxCPM_mid_najdi")

wav = model.generate(
    text="VoxCPM is an innovative end-to-end TTS model from ModelBest, designed to generate highly expressive speech.",
    prompt_wav_path=None,      # optional: path to a prompt speech for voice cloning
    prompt_text=None,          # optional: reference text
    cfg_value=2.0,             # LM guidance on LocDiT, higher for better adherence to the prompt, but maybe worse
    inference_timesteps=10,   # LocDiT inference timesteps, higher for better result, lower for fast speed
    normalize=True,           # enable external TN tool
    denoise=True,             # enable external Denoise tool
    retry_badcase=True,        # enable retrying mode for some bad cases (unstoppable)
    retry_badcase_max_times=3,  # maximum retrying times
    retry_badcase_ratio_threshold=6.0, # maximum length restriction for bad case detection (simple but effective), it could be adjusted for slow pace speech
)

sf.write("output.wav", wav, 16000)
print("saved: output.wav")

VoxCPM2 Najdi Arabic TTS (diacritized) โ€” final

Full fine-tune of VoxCPM2 (2.29B) for Saudi Najdi Arabic TTS, trained on diacritized text so that harakat control pronunciation at inference time.

This checkpoint: step 18264 โ€” final, end of epoch 1 (val loss 0.7848).

Why diacritics, and why a full fine-tune

VoxCPM2's tokenizer has no diacritic entries, so every harakat falls through to a byte-fallback pair (<0xD9><0x8E> = fatha) whose embedding rows carry a fusha/Quranic association from pretraining. Token count inflates x1.96 on marked text.

A LoRA run on this data could not fix that: apply_lora_to_named_linear_modules wraps Linear only (q/k/v/o_proj) and freezes everything else, so nn.Embedding is structurally untouchable โ€” attention can only reinterpret those rows in context, never change them. Full fine-tuning unfreezes everything except the AudioVAE, so the rows themselves retrain.

Training data

292,217 rows / 515.8 h, derived from a 377 h / ~1,032-speaker Najdi YouTube corpus:

portion rows note
diacritized 204,552 coverage mean 0.804
long-form (>25 s), bare 3,835 diacritization degrades past ~25 s
bare duplicates of diacritized clips 83,830 same audio, marks stripped

70.5% of rows carry marks; 45.0% carry a same-speaker ref_audio.

The bare duplicates are the load-bearing part. If bare and marked text sat on disjoint audio, the model could satisfy both by keying on the voice and learn nothing about marks. Identical audio carrying both forms leaves one consistent reading: marks are optional modifiers of the same speech.

Diacritics come from NAMAA-Space/Cohere-Speech-Tashkeel-2B, an acoustic diacritizer, so harakat reflect how each word was actually said rather than what a text model guesses โ€” it stays dialectal (ู†ูุชู’ุณูŽู„ูŽู‘ู‰, ุญูŽุชู’ู„ูŽุงู‚ููˆุง, ุญูŽูŠูŽุงุฎูุฐ). Because it runs ~10% letter WER its output is not used as the transcript: the corpus transcript is the letter backbone and only marks transfer word-by-word, leaving misheard words bare. Word-final marks (i'rab / pausal) are stripped; shadda is kept, being gemination rather than a case ending.

Transcripts are Cohere ASR text, not raw auto-captions.

Training

Stock openbmb/VoxCPM2, one epoch, 18,264 steps at effective batch 16 (4 x 4 grad-accum), lr 1e-5, weight_decay 0.01, warmup 100, max_grad_norm 1.0, lambdas 1.0/1.0. Single H200.

Validation loss: 0.9689 (step 0) -> 0.8033 (2k) -> 0.7852 (8k) -> 0.7834 (15k, best) -> 0.7848 (18k). Essentially flat past step 8,000.

Inference

Output is 48 kHz (model.tts_model.sample_rate), not 16 kHz โ€” writing it with a 16 kHz header plays ~3x slow and about an octave and a half low.

Short text works through the standard CLI:

voxcpm clone --model-path <this-repo> \
  --text "ูˆูุด ุฑูŽุงูŠููƒ ู†ูุชู’ู‚ูŽุงุจูŽู„ ุจูŽุงูƒูุฑ ุจูุงู„ู’ู…ูŽู‚ู’ู‡ูŽู‰ ุงู„ู„ูู‘ูŠ ุฌูŽู†ู’ุจ ุงู„ู’ุจูŽูŠู’ุช" \
  --reference-audio ref.wav --no-denoiser --output out.wav

Long text must be chunked. A single autoregressive pass past ~20 s degenerates into syllable babble in its tail. Measured on a 47 s single-shot generation: transcribe-back WER 0.41-0.48, with output like ุจูŠ ุจูŠ ุจูŠูŠุณูˆุดุงูŠุดุงูŠ ... ุจูŠุทุง ุจูŠุทุง ูˆูŠุทุจูŠ ุทุจูŠุทุจูŠุฑ. Insertions and deletions were both zero, so this is accumulated drift, not a stop-head failure.

Splitting on sentence boundaries fixes it:

split chunks transcribe-back WER
single-shot 47 s 1 0.410-0.476
comma + word fallback 8 0.105
full stops / question marks only 16 0.069

Chunks up to ~15 s measured clean (WER 0.040-0.100). cfg_value 2.0 beat 1.5 and 1.3 (0.250 vs 0.500) โ€” do not lower it.

Known limitations

  • Inference text must be diacritized in the same convention as training: pausal, word-final marks stripped, dialectal vowels. Text carrying standard MSA i'rab endings is off-distribution. There is no text-side diacritizer in this stack โ€” the one used to build the data is acoustic and needs audio.
  • Mark coverage is ~80%, so sentences are internally mixed (some words bare). That is realistic input, not a defect.
  • Reference audio in training is capped at 10.0 s; longer references are untested territory.
  • Trained on Najdi. The gaf (ู‚ pronounced /g/) is a deliberate register target, not an error.
Downloads last month
-
Safetensors
Model size
2B params
Tensor type
F32
ยท
Inference Providers NEW
This model isn't deployed by any Inference Provider. ๐Ÿ™‹ Ask for provider support

Model tree for Rabe3/VoxCPM_mid_najdi

Base model

openbmb/VoxCPM2
Finetuned
(30)
this model