File size: 8,849 Bytes
bb6f331
a4c8461
 
 
ab1991e
 
 
 
 
 
 
 
 
 
 
 
 
 
bb6f331
9db5c1f
63b5743
ab1991e
a4c8461
 
ab1991e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63b5743
ab1991e
63b5743
ab1991e
 
 
63b5743
ab1991e
 
 
63b5743
 
7e38e52
 
ab1991e
 
 
 
63b5743
 
 
 
 
 
 
 
 
 
ab1991e
 
 
 
 
 
 
 
 
0710da8
 
 
 
 
 
 
 
ab1991e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0de4612
 
 
 
 
 
 
 
 
 
 
ab1991e
 
 
 
 
 
0710da8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ab1991e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a4c8461
 
 
 
 
 
 
 
 
ab1991e
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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
---
license: other
license_name: fish-audio-research-license
license_link: https://github.com/maxmelichov/fish-speech/blob/main/LICENSE
language:
- he
- en
tags:
- text-to-speech
- tts
- hebrew
- fish-speech
- s2-pro
- lora
- voice-cloning
base_model: fishaudio/s2-pro
library_name: fish-speech
pipeline_tag: text-to-speech
---

<img src="fih.png" alt="Fish Audio S2-Pro Hebrew" width="420">

Built with Fish Audio.

# Fish Audio S2-Pro โ€” Hebrew (LoRA + atomic IPA tokens)

A Hebrew adapter for [`fishaudio/s2-pro`](https://huggingface.co/fishaudio/s2-pro).
It keeps the base model's multilingual ability and voice cloning intact, and adds
native Hebrew synthesis driven by **IPA** rather than nikud.

This repo contains **only the adapter** (~67M parameters) plus the extended
tokenizer. You still need the S2-Pro base weights and codec.

## What's here

| File | What it is |
|---|---|
| `hebrew_lora_step2200.safetensors` | LoRA deltas + the trained `ipa_embeddings` table (67M params, bf16) |
| `hebrew_lora_step2200.ckpt` | Same weights as a Lightning checkpoint, with optimizer state โ€” use this to resume training |
| `config.json` | S2-Pro config extended with `num_ipa_tokens: 26`, `ipa_token_start: 155774` |
| `ipa_token_map.json` | IPA symbol โ†’ atomic token (e.g. `สƒ` โ†’ `<ipa_u0283>`) |
| `ipa_embeddings.pt` | Initial IPA embedding table (mean of the symbol's BPE pieces); the trained one lives in the adapter |
| `tokenizer/` | S2-Pro tokenizer extended 155,774 โ†’ 155,800 tokens |
| `samples/` | Generated audio (see below) |

## Quick start

Code lives in the fork the adapter was trained with:

```bash
git clone https://github.com/maxmelichov/fish-speech
cd fish-speech && uv sync --python 3.12 --extra cu129
pip install renikud-plus                 # Hebrew grapheme-to-phoneme

bash tools/hebrew/setup_hebrew.sh        # base weights + this adapter + IPA checkpoint

python tools/hebrew/infer_hebrew.py \
    --text "ืฉืœื•ื, ืžื” ืฉืœื•ืžืš ื”ื™ื•ื?" \
    --lora-checkpoint checkpoints/hebrew/hebrew_lora_step2200.safetensors \
    --output out.wav
```

Add `--ref-audio my_voice.wav --ref-text "..."` to clone a voice.

`infer_hebrew.py` runs plain unvocalized Hebrew through
[RenikudPlus](https://github.com/maxmelichov/RenikudPlus) G2P, maps the
IPA to the atomic tokens, and chunks long inputs on sentence boundaries.
`--lora-scale` scales the delta (0.0 = pure base model) if you want to dial the
adaptation down.

**Fine-tuning on your own Hebrew data** is one command โ€” a directory per speaker
of `*.wav` plus sibling `.lab` transcripts:

```bash
AUDIO_ROOT=my_audio tools/hebrew/run_hebrew_pipeline.sh
```

See [`tools/hebrew/README.md`](https://github.com/maxmelichov/fish-speech/blob/main/tools/hebrew/README.md)
for the full guide.

## How it works

**Atomic IPA tokens.** S2-Pro's BPE splits IPA into pieces that collide with
English orthography โ€” Hebrew `ื™` phonemized as `j` was read as the English letter
*jay*. So each of the 26 Hebrew IPA symbols gets a dedicated input-only token
(`<ipa_j>`, `<ipa_u0283>`, โ€ฆ) in a separate trainable `nn.Embedding`, initialized
to the mean of the symbol's original BPE pieces. The output vocabulary is
untouched โ€” these tokens are never predicted, only read.

**What trains.** LoRA r=32, ฮฑ=16 on `attention` + `mlp`, plus the IPA embedding
table โ€” 66.9M parameters total: 60.2M in the slow transformer, 6.7M in the fast
transformer, 0.03M IPA embeddings. Frozen are the direct interfaces to codebook
space โ€” `fast_embeddings`, `fast_output`, and the tied slow embeddings/output โ€”
which is what keeps timbre close to the base model. Note ฮฑ/r = 0.5, not the usual
2.0; see *Caveats*. The residual-codebook loss is down-weighted to 0.3
(Qwen3-TTS's sub-talker coefficient) so the gradient stays on the textโ†’semantic
mapping.

**Training.** 279,476 Hebrew utterances (~10 speakers, WER โ‰ค 0.1), reference-
conditioned on a same-speaker utterance 80% of the time so training prompts match
the exact `generate_long()` inference format. bf16, lr 5e-5 constant with 100-step
warmup, effective batch 12, 2200 optimizer steps.

## Upstream bug fixed along the way

S2-Pro sets `scale_codebook_embeddings=True`. At inference, `forward_generate()`
divides semantic-position embeddings by `sqrt(num_codebooks + 1)` = 3.317; the
training path in `embed()` did **not**. Every fine-tune therefore learned against
embeddings 3.3ร— larger than the ones it would see at generation time. Teacher-
forced CE looked fine while free-running generation collapsed after the first
word โ€” the classic symptom in fishaudio/fish-speech issues
[#1136](https://github.com/fishaudio/fish-speech/issues/1136) (Japanese gibberish),
[#682](https://github.com/fishaudio/fish-speech/issues/682) (Hindi noise) and
[#814](https://github.com/fishaudio/fish-speech/issues/814).

Five Hebrew runs collapsed the same way before this was found. After the fix
(train and inference embeddings verified bit-identical):

| | sample RMS | energy decay over the utterance |
|---|---|---|
| before | 0.008 โ€“ 0.022 | 0.07ร— |
| after | 0.171 โ€“ 0.205 | 1.02ร— |
| base model reference | 0.181 | โ€” |

The fix is in `fish_speech/models/text2semantic/llama.py` in the fork above and
applies to any S2-Pro fine-tune, Hebrew or not.

## Samples

- `00_base_out_of_the_box.wav` โ€” **stock `fishaudio/s2-pro`, no adapter, no
  G2P** โ€” plain Hebrew script straight in. S2-Pro is multilingual and does
  produce Hebrew-*shaped* speech, but it isn't accurate: this sample of
  "ืฉืœื•ื, ืžื” ืฉืœื•ืžืš ื”ื™ื•ื?" ("hello, how are you today?") comes out as "ืกืœื•ื
  ืžื”ืฉืœื•ื ื—ื™ื•ื" โ€” ืฉืœื•ื โ†’ ืกืœื•ื, ืฉืœื•ืžืš garbled into ืžืฉืœื•ื. Not a cherry-pick: an
  8-seed sweep on a different sentence in this fork's eval found the same
  failure every time, and a stock-model WER of 0.383 across 11 sentences,
  worse than real human speech scores on the same metric. This is the gap
  the adapter below closes.

The rest of `samples/` is generated **with this adapter**:

- `01_podcast_2hosts_63s.wav` โ€” 63s two-host Hebrew conversation, cloned voices
- `03_longform_15s.wav` โ€” multi-sentence long-form
- `04_yod_BASE.wav` / `05_yod_LORA.wav` โ€” the `ื™` โ†’ English *jay* failure, before and after atomic IPA tokens
- `07_clone_LORA_ranlevi.wav` โ€” voice clone from a real Hebrew speaker reference

## Caveats โ€” this checkpoint is early, not final

- **Undertrained, and stopped by hand.** 2,200 optimizer steps โ‰ˆ 53k utterances
  seen, under 20% of one epoch over the 279k-row set. Train loss was still falling
  (3.66 โ†’ 2.70 base CE) and val loss was still improving monotonically at every
  checkpoint (2.934 โ†’ 2.844 โ†’ 2.820 โ†’ 2.807). Nothing had plateaued; the run was
  simply halted.
- **ฮฑ/r = 0.5 is a workaround for a bug that no longer exists.** The unusual
  scaling was chosen empirically because at ฮฑ=64 the delta destroyed free-running
  generation โ€” which we now know was the embedding-scale bug above, not the LoRA
  strength. That rationale is obsolete post-fix, and the standard ฮฑ = 2r was never
  re-tried. It may well be better.
- The pitch homogenization noted below is the symptom you would expect from
  putting LoRA on the fast transformer at all. Freezing `fast_layers` entirely is
  the obvious next experiment.

## Known limitations

- **Emotion tags (`[whisper]`, `[excited]`, โ€ฆ) do not work** โ€” and this is not a
  regression from the LoRA. Measured on the *base* model in *English*: plain /
  whisper / shouting produced RMS 0.0655 / 0.0652 / 0.0689, i.e. no response at
  all. The released S2-Pro weights simply lack the tag alignment.
- **Pitch is not cloned.** Timbre transfers well (4/4 by ear), but neither base nor
  LoRA reproduces the reference's F0 (base mean |err| 23 Hz, LoRA 20 Hz). The LoRA
  homogenizes pitch somewhat: spread across speakers drops from 66 Hz to 26 Hz.
- Trained on read/broadcast-style Hebrew; conversational and heavily accented
  speech are out of distribution.
- Hebrew input must go through G2P. Feeding nikud or bare Hebrew script directly
  to the model is out of distribution โ€” use `infer_hebrew.py`, which handles it.

## License

S2-Pro (the base model this adapter is trained on) is released under the
**[Fish Audio Research License](https://github.com/maxmelichov/fish-speech/blob/main/LICENSE)**
โ€” *not* CC BY-NC-SA 4.0, correcting an earlier version of this card. This
adapter is a Derivative Work under that license and inherits its terms:
research and non-commercial use only (personal use, evaluation, academic
work); any commercial use requires a separate license directly from Fish
Audio (business@fish.audio). See `NOTICE` and `LICENSE` in this repo for the
full text and the required attribution.

Do not use it to clone a voice you do not have permission to clone.