Text-to-Speech
Transformers
TensorBoard
Safetensors
Bulgarian
omnivoice
tts
bulgarian
voice-cloning
prosody

OmniVoice-BG

A Bulgarian text-to-speech model, fine-tuned from k2-fsa/OmniVoice. It produces natural, native-sounding Bulgarian speech, removing the foreign accent that stock OmniVoice has on Bulgarian — and, in this revision, it renders imperative (final-stress) verb forms correctly on voices it has never heard.

This revision (ft9b) replaces the narration-only release of June 2026. The older weights are still available on the v1-narration-2026-06 branch (revision="v1-narration-2026-06") if you depend on them. The change is additive in intent — it is the same narration model, fine-tuned further on imperative stress — but general narration quality was not re-benchmarked, so if narration is all you need and you have validated the old weights, there is no reason to move.

These are the fine-tuned weights only. The full OmniVoice-BG system has two more serving-side pillars — a lexical stress front-end and deterministic fixed-voice cloning — which are not baked into the weights.

What this revision fixes, and what it does not

Bulgarian marks grammatical role with stress position, and the same spelling takes different stress depending on that role: хо́ди "(s)he walks" against ходи́ "walk!". Getting the imperative right had defeated four previous fine-tunes (ft4–ft8): stress transferred only to voices present in training, because from a handful of clips per (verb, voice) the model memorised timbre rather than abstracting a text→stress rule.

Fixed. For ходи, this checkpoint renders both roles correctly on held-out reference voices — ходи́ as an imperative and хо́ди as a 3rd-person present. That it gets both right is the point: it learned where the mark goes, not a flipped default.

Not fixed. For прави, the imperative is still wrong on male and mid-pitch voices (correct on female). The cause is measured, not guessed: imperative exposure in the training mix is near-identical between the two verbs (ходи 504 rows, прави 488), but stem exposure differs 12-fold — хо́ди 55 against пра́ви 660, because пра́ви "does" saturates ordinary narration. So ходи́ wins its stress-role contest 9.2:1 while прави́ is out-voted 0.7:1. A follow-up run that rebalanced прави to 3.4:1 exists but is not released, because it has not been fully validated by ear.

Practical consequence: do not rely on this model for imperative-heavy copy without listening. Non-resistant imperatives (обади́, купи́, наслади́, вдигни́) were already reliable and remain so.

Model details

  • Architecture: OmniVoice — a diffusion/mask-prediction language model predicting Higgs Audio V2 codec tokens (8 codebooks @ 24 kHz). Backbone Qwen/Qwen3-0.6B.
  • Base model: k2-fsa/OmniVoice (Apache-2.0)
  • Lineage: stock OmniVoice → Bulgarian narration (audiobooks) → ft9b, this revision
  • Sample rate: 24 kHz mono · Size: ~2.45 GB (model.safetensors)

Training

Fine-tuned for 3,000 steps (38 min on one 16 GB GPU) from the Bulgarian narration checkpoint. Full config in train_config.json, data mix in data_config.json.

Setting Value
init from Bulgarian narration checkpoint (step 6500)
steps 3000
learning rate 5e-6, cosine, 5% warmup
batch batch_tokens=1024, grad_accum=8, max_batch_size=8
precision bf16 mixed · attention SDPA
weight decay 0.01 · seed 42

Data mix

Five token streams — two of ordinary narration, three of imperatives. Narration stays dominant by volume; the imperative streams carry high repeat weights because they are tiny. All text is stress-marked (vowel + U+0301).

Stream Source Size repeat
Narration, main beleata74/bulgarian-audiobooks-tts-400h — single female narrator, 24 kHz, MIT 193,768 clips ≈ 448 h 1× plain + 4× sparse-reference twin
Narration, base raditotev/bg-audiobooks-tts — single narrator, CC0 10,095 clips ≈ 14.4 h 1× plain + 4× sparse-reference twin
Imperatives, synthetic Teacher distillation from beleata74/BgTTS-38M-V2, labelled by a Kaldi forced aligner; 164 verbs × 22 frames, 4 reference voices 6,648 clips 8×
Imperatives, human Own recordings, one voice — the two verbs the teacher cannot say (прави́ / ходи́) 30 clips 12×
Imperatives, volunteer 11 real speakers, 483 accepted takes, head speakers capped and tail voices sox-perturbed into pseudo-speakers 868 rows 4×

Dev set: the sparse-reference dev splits of the two narration corpora.

Two notes on why the mix looks like this. The sparse-reference twins are the same audio re-tokenised in the short-prompt style the serving layer uses, weighted 4× so the model is good at the condition it actually runs in. And прави/ходи are deliberately absent from the synthetic set: the BgTTS teacher renders them with stem stress, so including them would have taught the error — they come from the human and volunteer streams instead.

The volunteer stream is the component that made the difference. Earlier fine-tunes (ft4–ft8) had the synthetic and human sets but not many real timbres per verb, and imperative stress stayed voice-specific. The measured lesson was that the gain tracks distinct-voice exposure per verb, not clip count, which is why this revision exempts the resistant verbs from the head-speaker cap (прави 12 → 72 rows / 4 voices, ходи 20 → 81 rows / 5 voices).

The narration also enters a second time, through the initialisation: step 6500 of the 413 h Bulgarian narration run, trained on the same two corpora.

Why the released checkpoint is not the lowest-loss one

Validation loss over the run is flat and noisy — 3.738 to 3.812 across all twelve eval points, best at step 1250, with the released step-3000 checkpoint at 3.786. Eval loss does not track stress correctness at this scale. The checkpoint was selected by blind listening on held-out reference voices, with each verb rendered in both grammatical roles so that a gain in the imperative could not hide a regression in the 3rd-person present. TensorBoard logs are in tensorboard/.

Usage

pip install omnivoice

omnivoice-infer \
    --model preslaff/omnivoice-bg \
    --text "Ходи́ на разходка всяка сутрин." \
    --ref_audio narrator.wav \
    --output out.wav
from omnivoice import OmniVoice

model = OmniVoice.from_pretrained("preslaff/omnivoice-bg", train=False)
wav = model.generate(
    text="Ходи́ на разходка всяка сутрин.",   # stress marked with U+0301
    ref_audio="narrator.wav",                # fixed reference for consistent cloning
)

Mark the stress. This model is trained on text carrying combining acute accents (vowel + U+0301) and will not infer Bulgarian lexical stress from bare text. Pass marked text, use a fixed reference clip, and seed decoding for reproducibility — the OmniVoice-BG server does all three automatically.

Related

License & attribution

Apache-2.0, a derivative of OmniVoice by k2-fsa (Apache-2.0); base model and architecture © Xiaomi Corp. Narration data: raditotev/bg-audiobooks-tts (CC0) and beleata74/bulgarian-audiobooks-tts-400h (MIT). The synthetic imperative set was distilled from beleata74/BgTTS-38M-V2 (Apache-2.0).

The volunteer imperative recordings that made this revision possible are not published — the speakers consented to their voices being used for training, not to redistribution of the audio, and no contact details were ever collected. Thank you to the 11 people who recorded them.

Downloads last month
50
Safetensors
Model size
0.6B params
Tensor type
I64
·
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for preslaff/omnivoice-bg

Finetuned
Qwen/Qwen3-0.6B
Finetuned
k2-fsa/OmniVoice
Finetuned
(70)
this model

Datasets used to train preslaff/omnivoice-bg