File size: 4,704 Bytes
fa87a31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
---
license: apache-2.0
language:
- ar
base_model: openbmb/VoxCPM2
pipeline_tag: text-to-speech
tags:
- text-to-speech
- arabic
- najdi
- saudi
- voxcpm
- diacritics
- tashkeel
---

# 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](https://huggingface.co/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:

```bash
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.