初始化项目,由ModelHub XC社区提供模型
Model: patrickcmd/orpheus-3b-tts-multilingual Source: Original Platform
This commit is contained in:
658
README.md
Normal file
658
README.md
Normal file
@@ -0,0 +1,658 @@
|
||||
---
|
||||
license: apache-2.0
|
||||
language:
|
||||
- ach
|
||||
- af
|
||||
- en
|
||||
- ee
|
||||
- ff
|
||||
- ha
|
||||
- ig
|
||||
- ki
|
||||
- rw
|
||||
- lgg
|
||||
- ln
|
||||
- lg
|
||||
- luo
|
||||
- nyn
|
||||
- st
|
||||
- sw
|
||||
- teo
|
||||
- tn
|
||||
- xh
|
||||
- yo
|
||||
library_name: transformers
|
||||
pipeline_tag: text-to-speech
|
||||
tags:
|
||||
- text-to-speech
|
||||
- tts
|
||||
- orpheus
|
||||
- multilingual
|
||||
- multi-speaker
|
||||
- african-languages
|
||||
- low-resource-language
|
||||
- sunbird
|
||||
- snac
|
||||
- unsloth
|
||||
- llama
|
||||
datasets:
|
||||
- Sunbird/tts
|
||||
base_model: unsloth/orpheus-3b-0.1-pretrained
|
||||
---
|
||||
|
||||
# Orpheus-3B Sunbird Multilingual TTS
|
||||
|
||||
A multilingual, multi-speaker text-to-speech model fine-tuned from
|
||||
[`unsloth/orpheus-3b-0.1-pretrained`](https://huggingface.co/unsloth/orpheus-3b-0.1-pretrained)
|
||||
on the **full** [`Sunbird/tts`](https://huggingface.co/datasets/Sunbird/tts)
|
||||
corpus — 20 language configurations and every speaker present in the
|
||||
dataset.
|
||||
|
||||
The model accepts arbitrary text and emits 24 kHz mono speech via the
|
||||
[SNAC](https://huggingface.co/hubertsiuzdak/snac_24khz) audio codec.
|
||||
Voice selection happens at the prompt level: prepend the chosen
|
||||
`speaker_id` followed by `": "` to your text, and the model produces
|
||||
audio in that speaker's voice.
|
||||
|
||||
## Quick links
|
||||
|
||||
- **Base model:** [`unsloth/orpheus-3b-0.1-pretrained`](https://huggingface.co/unsloth/orpheus-3b-0.1-pretrained) (Llama-3 architecture)
|
||||
- **Audio codec:** [`hubertsiuzdak/snac_24khz`](https://huggingface.co/hubertsiuzdak/snac_24khz) (24 kHz, 7 codes per ~12 ms frame)
|
||||
- **Training dataset:** [`Sunbird/tts`](https://huggingface.co/datasets/Sunbird/tts) — all 20 configs, all speakers
|
||||
- **Training framework:** [Unsloth](https://github.com/unslothai/unsloth) + HuggingFace Trainer
|
||||
|
||||
## Languages covered
|
||||
|
||||
Speaker IDs encode both the source corpus (`salt_*`, `waxal_*`, `slr32_*`,
|
||||
`slr129_*`, `bateesa_*`) and the language. Languages marked with an em dash
|
||||
in the Speaker IDs column are present in the model's training mix but do
|
||||
not currently expose individual voice IDs in this checkpoint.
|
||||
|
||||
| Config | Language | ISO 639-1 | Region | Speaker IDs |
|
||||
|---|---|---|---|---|
|
||||
| `ach` | Acholi | — | Uganda, South Sudan | `salt_ach_0001`<br>`waxal_ach_0001`<br>`waxal_ach_0005`<br>`waxal_ach_0006`<br>`waxal_ach_0008` |
|
||||
| `afr` | Afrikaans | af | South Africa, Namibia | `slr32_afr_0009` |
|
||||
| `eng` | English | en | (control language) | `salt_eng_0001`<br>`salt_eng_0002`<br>`salt_eng_0003` |
|
||||
| `ewe` | Ewe | ee | Ghana, Togo | `slr129_ewe_0001` |
|
||||
| `ful` | Fulah | ff | West Africa (Sahel) | `waxal_ful_0003`<br>`waxal_ful_0004`<br>`waxal_ful_0006` |
|
||||
| `hau` | Hausa | ha | Nigeria, Niger, Chad | `waxal_hau_0004`<br>`waxal_hau_0006`<br>`waxal_hau_0007`<br>`waxal_hau_0008` |
|
||||
| `ibo` | Igbo | ig | Nigeria | `waxal_ibo_0003`<br>`waxal_ibo_0005`<br>`waxal_ibo_0008` |
|
||||
| `kik` | Kikuyu | ki | Kenya | `waxal_kik_0003`<br>`waxal_kik_0004` |
|
||||
| `kin` | Kinyarwanda | rw | Rwanda | `bateesa_kin_0001` |
|
||||
| `lgg` | Lugbara | — | Uganda, DRC | — |
|
||||
| `lin` | Lingala | ln | DRC, Republic of Congo | `slr129_lin_0001` |
|
||||
| `lug` | Luganda | lg | Uganda | `salt_lug_0001`<br>`waxal_lug_0002`<br>`waxal_lug_0003`<br>`waxal_lug_0004`<br>`waxal_lug_0005`<br>`waxal_lug_0006`<br>`waxal_lug_0007`<br>`waxal_lug_0008` |
|
||||
| `luo` | Luo (Dholuo) | — | Kenya, Tanzania | `waxal_luo_0001`<br>`waxal_luo_0002`<br>`waxal_luo_0003`<br>`waxal_luo_0004` |
|
||||
| `nyn` | Runyankole | — | Uganda | `salt_nyn_0001`<br>`waxal_nyn_0003`<br>`waxal_nyn_0004`<br>`waxal_nyn_0007`<br>`waxal_nyn_0008` |
|
||||
| `sot` | Sesotho | st | Lesotho, South Africa | — |
|
||||
| `swa` | Swahili | sw | East Africa | `waxal_swa_0006`<br>`waxal_swa_0007` |
|
||||
| `teo` | Ateso | — | Uganda, Kenya | `salt_teo_0001` |
|
||||
| `tsn` | Setswana | tn | Botswana, South Africa | — |
|
||||
| `xho` | Xhosa | xh | South Africa | `slr32_xho_0012` |
|
||||
| `yor` | Yoruba | yo | Nigeria, Benin | `waxal_yor_0002`<br>`waxal_yor_0006`<br>`waxal_yor_0008` |
|
||||
|
||||
Per-language quality scales with the amount of training data Sunbird
|
||||
collected for that language; some configs have many more speaker hours
|
||||
than others. **Audition the test split** for each language before relying
|
||||
on a particular speaker — see the discovery snippet below.
|
||||
|
||||
## TL;DR
|
||||
|
||||
```python
|
||||
# After installing the dependencies (see "Inference" below)
|
||||
wav = synthesize("Mwattu, oli otya?", speaker_id="salt_lug_0001") # Luganda
|
||||
wav = synthesize("Habari yako rafiki.", speaker_id="salt_swa_0001") # Swahili
|
||||
wav = synthesize("Bawo ni, ọrẹ mi?", speaker_id="salt_yor_0001") # Yoruba
|
||||
```
|
||||
|
||||
The model has no explicit "language" knob — the language identity
|
||||
travels via the speaker tag, since each `salt_<lang>_<NNNN>` voice was
|
||||
recorded in exactly one language.
|
||||
|
||||
---
|
||||
|
||||
## Discovering speaker IDs
|
||||
|
||||
The exact speaker_ids in each config can be enumerated from the dataset:
|
||||
|
||||
```python
|
||||
from collections import defaultdict
|
||||
from datasets import load_dataset, get_dataset_config_names
|
||||
|
||||
CONFIGS = get_dataset_config_names("Sunbird/tts") # the 20 languages
|
||||
|
||||
speakers_by_lang = defaultdict(set)
|
||||
for cfg in CONFIGS:
|
||||
ds = load_dataset("Sunbird/tts", cfg, split="train")
|
||||
for sid in ds["speaker_id"]:
|
||||
speakers_by_lang[cfg].add(sid)
|
||||
|
||||
for cfg, sids in sorted(speakers_by_lang.items()):
|
||||
print(f"{cfg}: {len(sids)} speaker(s) — {sorted(sids)[:3]}{'...' if len(sids) > 3 else ''}")
|
||||
```
|
||||
|
||||
Speaker IDs follow the pattern `salt_<lang>_<NNNN>` (e.g.,
|
||||
`salt_lug_0001`, `salt_ach_0007`). Pass any one of them as
|
||||
`speaker_id` to either inference function below.
|
||||
|
||||
---
|
||||
|
||||
## Inference
|
||||
|
||||
The model wraps every prompt in a multi-speaker tagged format:
|
||||
|
||||
```
|
||||
[SOH] + tokenize("<speaker_id>: <your text>") + [EOT, EOH]
|
||||
```
|
||||
|
||||
and the model autoregressively emits Llama-3 special tokens followed by
|
||||
SNAC audio codes that decode to a 24 kHz waveform. Two reference
|
||||
implementations follow.
|
||||
|
||||
### Option A — `transformers` + `unsloth` (single request)
|
||||
|
||||
Best for development, notebook-driven iteration, and small batch sizes.
|
||||
|
||||
**Install:**
|
||||
|
||||
```bash
|
||||
pip install unsloth snac soundfile torchcodec "datasets>=3.4.1,<4.0.0"
|
||||
```
|
||||
|
||||
**Run:**
|
||||
|
||||
```python
|
||||
import os
|
||||
import numpy as np
|
||||
import torch
|
||||
import soundfile as sf
|
||||
from unsloth import FastLanguageModel
|
||||
from snac import SNAC
|
||||
|
||||
MODEL_ID = "sunbird/orpheus-3b-tts-multilingual"
|
||||
|
||||
# Special tokens — must match the training format
|
||||
END_OF_TEXT = 128009
|
||||
START_OF_SPEECH = 128257
|
||||
END_OF_SPEECH = 128258
|
||||
START_OF_HUMAN = 128259
|
||||
END_OF_HUMAN = 128260
|
||||
PAD_TOKEN = 128263
|
||||
AUDIO_TOKEN_LO = 128266
|
||||
AUDIO_TOKEN_HI = 128266 + 7 * 4096 # exclusive
|
||||
|
||||
# 1) Load the LM (LoRA already merged into 16-bit weights at training time)
|
||||
model, tokenizer = FastLanguageModel.from_pretrained(
|
||||
model_name = MODEL_ID,
|
||||
max_seq_length = 4096,
|
||||
dtype = None, # auto bf16 / fp16
|
||||
load_in_4bit = False, # set True to halve VRAM at slight quality cost
|
||||
token = os.environ.get("HF_TOKEN"),
|
||||
)
|
||||
FastLanguageModel.for_inference(model)
|
||||
|
||||
# 2) Load SNAC decoder (CPU is fine — frees GPU for the LM)
|
||||
snac_model = SNAC.from_pretrained("hubertsiuzdak/snac_24khz").to("cpu")
|
||||
|
||||
|
||||
def _redistribute_codes(code_list: list[int]) -> torch.Tensor:
|
||||
layer_1, layer_2, layer_3 = [], [], []
|
||||
for i in range(len(code_list) // 7):
|
||||
layer_1.append(code_list[7*i])
|
||||
layer_2.append(code_list[7*i + 1] - 4096)
|
||||
layer_3.append(code_list[7*i + 2] - 2*4096)
|
||||
layer_3.append(code_list[7*i + 3] - 3*4096)
|
||||
layer_2.append(code_list[7*i + 4] - 4*4096)
|
||||
layer_3.append(code_list[7*i + 5] - 5*4096)
|
||||
layer_3.append(code_list[7*i + 6] - 6*4096)
|
||||
if not layer_1:
|
||||
return torch.zeros(1, 1, 12000) # ~0.5s silence fallback
|
||||
clamp = lambda vals: [max(0, min(4095, v)) for v in vals]
|
||||
codes = [torch.tensor(clamp(layer_1)).unsqueeze(0),
|
||||
torch.tensor(clamp(layer_2)).unsqueeze(0),
|
||||
torch.tensor(clamp(layer_3)).unsqueeze(0)]
|
||||
return snac_model.decode(codes)
|
||||
|
||||
|
||||
def synthesize(text: str, speaker_id: str,
|
||||
*, max_new_tokens: int = 1200,
|
||||
temperature: float = 0.6, top_p: float = 0.95,
|
||||
repetition_penalty: float = 1.1,
|
||||
seed: int | None = None) -> np.ndarray:
|
||||
"""Synthesize speech for `text` in the voice of `speaker_id`.
|
||||
|
||||
`speaker_id` must be one of the speakers seen during training,
|
||||
e.g. "salt_lug_0001" (Luganda) or "salt_swa_0003" (Swahili).
|
||||
"""
|
||||
if seed is not None:
|
||||
torch.manual_seed(seed)
|
||||
|
||||
tagged = f"{speaker_id}: {text}"
|
||||
text_ids = tokenizer(tagged, return_tensors="pt").input_ids
|
||||
soh = torch.tensor([[START_OF_HUMAN]], dtype=torch.int64)
|
||||
end = torch.tensor([[END_OF_TEXT, END_OF_HUMAN]], dtype=torch.int64)
|
||||
input_ids = torch.cat([soh, text_ids, end], dim=1).to("cuda")
|
||||
attention_mask = torch.ones_like(input_ids)
|
||||
|
||||
generated = model.generate(
|
||||
input_ids = input_ids, attention_mask = attention_mask,
|
||||
max_new_tokens = max_new_tokens,
|
||||
do_sample = True,
|
||||
temperature = temperature, top_p = top_p,
|
||||
repetition_penalty = repetition_penalty,
|
||||
eos_token_id = END_OF_SPEECH, use_cache = True,
|
||||
)
|
||||
|
||||
# Crop on last SOS, filter to audio token range, redistribute, decode
|
||||
sos_indices = (generated == START_OF_SPEECH).nonzero(as_tuple=True)
|
||||
cropped = generated[:, sos_indices[1][-1].item() + 1:] if len(sos_indices[1]) > 0 else generated
|
||||
row = cropped[0]
|
||||
audio_only = row[(row >= AUDIO_TOKEN_LO) & (row < AUDIO_TOKEN_HI)]
|
||||
n = (audio_only.size(0) // 7) * 7
|
||||
code_list = [t.item() - AUDIO_TOKEN_LO for t in audio_only[:n]]
|
||||
waveform = _redistribute_codes(code_list)
|
||||
return waveform.detach().squeeze().to("cpu").numpy().astype(np.float32)
|
||||
|
||||
|
||||
# 3) Use it — pick a speaker per language
|
||||
wav = synthesize("Mwattu, Mukama yeebazibwe.", speaker_id="salt_lug_0001", seed=42)
|
||||
sf.write("luganda.wav", wav, 24000)
|
||||
|
||||
wav = synthesize("Habari yako rafiki.", speaker_id="salt_swa_0001", seed=42)
|
||||
sf.write("swahili.wav", wav, 24000)
|
||||
```
|
||||
|
||||
### Option B — `vllm` (high throughput, batched, deployment)
|
||||
|
||||
Best for serving traffic. PagedAttention + continuous batching gives
|
||||
roughly **5–10× faster** single-request latency and **10–100× higher**
|
||||
throughput on batched requests vs. the `transformers` path. Multi-speaker
|
||||
batching (different `speaker_id`s in one call) gets the full benefit.
|
||||
|
||||
> **Important:** vLLM ships its own torch/transformers and conflicts
|
||||
> with Unsloth's pinned versions. Use a fresh Python environment for
|
||||
> vLLM serving — do not install on top of an Unsloth env.
|
||||
|
||||
**Install:**
|
||||
|
||||
```bash
|
||||
pip install vllm snac soundfile torchcodec "datasets>=3.4.1,<4.0.0"
|
||||
```
|
||||
|
||||
**Run:**
|
||||
|
||||
```python
|
||||
import os
|
||||
import numpy as np
|
||||
import torch
|
||||
import soundfile as sf
|
||||
from snac import SNAC
|
||||
from transformers import AutoTokenizer
|
||||
from vllm import LLM, SamplingParams
|
||||
|
||||
MODEL_ID = "sunbird/orpheus-3b-tts-multilingual"
|
||||
|
||||
END_OF_TEXT = 128009
|
||||
START_OF_SPEECH = 128257
|
||||
END_OF_SPEECH = 128258
|
||||
START_OF_HUMAN = 128259
|
||||
END_OF_HUMAN = 128260
|
||||
AUDIO_TOKEN_LO = 128266
|
||||
AUDIO_TOKEN_HI = 128266 + 7 * 4096
|
||||
|
||||
# 1) Load
|
||||
llm = LLM(
|
||||
model = MODEL_ID,
|
||||
dtype = "bfloat16",
|
||||
max_model_len = 4096,
|
||||
gpu_memory_utilization = 0.85,
|
||||
)
|
||||
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, token=os.environ.get("HF_TOKEN"))
|
||||
snac_model = SNAC.from_pretrained("hubertsiuzdak/snac_24khz").to("cpu")
|
||||
|
||||
|
||||
def _build_prompt_token_ids(text: str, speaker_id: str) -> list[int]:
|
||||
tagged = f"{speaker_id}: {text}"
|
||||
text_ids = tokenizer.encode(tagged, add_special_tokens=True)
|
||||
return [START_OF_HUMAN] + text_ids + [END_OF_TEXT, END_OF_HUMAN]
|
||||
|
||||
|
||||
def _codes_to_waveform(generated_token_ids: list[int]) -> np.ndarray:
|
||||
ids = torch.tensor(generated_token_ids, dtype=torch.int64)
|
||||
sos_pos = (ids == START_OF_SPEECH).nonzero(as_tuple=True)[0]
|
||||
if len(sos_pos) > 0:
|
||||
ids = ids[sos_pos[-1].item() + 1:]
|
||||
audio = ids[(ids >= AUDIO_TOKEN_LO) & (ids < AUDIO_TOKEN_HI)]
|
||||
n = (audio.size(0) // 7) * 7
|
||||
cl = [t.item() - AUDIO_TOKEN_LO for t in audio[:n]]
|
||||
l1, l2, l3 = [], [], []
|
||||
for i in range(len(cl) // 7):
|
||||
l1.append(cl[7*i])
|
||||
l2.append(cl[7*i+1] - 4096); l3.append(cl[7*i+2] - 2*4096)
|
||||
l3.append(cl[7*i+3] - 3*4096); l2.append(cl[7*i+4] - 4*4096)
|
||||
l3.append(cl[7*i+5] - 5*4096); l3.append(cl[7*i+6] - 6*4096)
|
||||
if not l1:
|
||||
return np.zeros(12000, dtype=np.float32)
|
||||
cb = lambda v: [max(0, min(4095, x)) for x in v]
|
||||
codes = [torch.tensor(cb(l1)).unsqueeze(0),
|
||||
torch.tensor(cb(l2)).unsqueeze(0),
|
||||
torch.tensor(cb(l3)).unsqueeze(0)]
|
||||
return snac_model.decode(codes).detach().squeeze().cpu().numpy().astype(np.float32)
|
||||
|
||||
|
||||
def synthesize(text: str, speaker_id: str,
|
||||
*, max_tokens: int = 1200,
|
||||
temperature: float = 0.6, top_p: float = 0.95,
|
||||
repetition_penalty: float = 1.1,
|
||||
seed: int | None = None) -> np.ndarray:
|
||||
sp = SamplingParams(
|
||||
temperature = temperature, top_p = top_p,
|
||||
repetition_penalty = repetition_penalty,
|
||||
max_tokens = max_tokens,
|
||||
stop_token_ids = [END_OF_SPEECH],
|
||||
skip_special_tokens = False,
|
||||
seed = seed,
|
||||
)
|
||||
pids = _build_prompt_token_ids(text, speaker_id)
|
||||
out = llm.generate([{"prompt_token_ids": pids}], sp)
|
||||
return _codes_to_waveform(list(out[0].outputs[0].token_ids))
|
||||
|
||||
|
||||
def synthesize_batch(items: list[dict], **kwargs) -> list[np.ndarray]:
|
||||
"""items: list of {"text": str, "speaker_id": str} — different speakers
|
||||
can be mixed in one batch."""
|
||||
sp = SamplingParams(
|
||||
temperature = kwargs.get("temperature", 0.6),
|
||||
top_p = kwargs.get("top_p", 0.95),
|
||||
repetition_penalty = kwargs.get("repetition_penalty", 1.1),
|
||||
max_tokens = kwargs.get("max_tokens", 1200),
|
||||
stop_token_ids = [END_OF_SPEECH],
|
||||
skip_special_tokens = False,
|
||||
seed = kwargs.get("seed"),
|
||||
)
|
||||
prompts = [{"prompt_token_ids": _build_prompt_token_ids(it["text"], it["speaker_id"])}
|
||||
for it in items]
|
||||
outputs = llm.generate(prompts, sp)
|
||||
return [_codes_to_waveform(list(o.outputs[0].token_ids)) for o in outputs]
|
||||
|
||||
|
||||
# 2) Single — pick a speaker per language
|
||||
wav = synthesize("Mwattu, oli otya?", speaker_id="salt_lug_0001", seed=42)
|
||||
sf.write("luganda.wav", wav, 24000)
|
||||
|
||||
# 3) Batched — different languages and speakers in one GPU pass
|
||||
items = [
|
||||
{"text": "Mwattu, oli otya?", "speaker_id": "salt_lug_0001"},
|
||||
{"text": "Habari yako rafiki.", "speaker_id": "waxal_swa_0006"},
|
||||
{"text": "Bawo ni, ọrẹ mi?", "speaker_id": "waxal_yor_0002"},
|
||||
{"text": "Sannu, ina kwana?", "speaker_id": "waxal_hau_0004"},
|
||||
{"text": "Goeie môre, hoe gaan dit?", "speaker_id": "slr32_afr_0009"},
|
||||
]
|
||||
wavs = synthesize_batch(items, seed=123)
|
||||
for i, (it, w) in enumerate(zip(items, wavs)):
|
||||
sf.write(f"batch_{i:02d}_{it['speaker_id']}.wav", w, 24000)
|
||||
```
|
||||
|
||||
### Generation parameters
|
||||
|
||||
| Param | Default | What it does |
|
||||
|---|---|---|
|
||||
| `temperature` | 0.6 | Lower = more deterministic, slightly flatter prosody. |
|
||||
| `top_p` | 0.95 | Nucleus sampling. Don't drop below 0.9 — produces robotic audio. |
|
||||
| `repetition_penalty` | 1.1 | Discourages stuck-on-one-frame artefacts. 1.0 disables it. |
|
||||
| `max_new_tokens` / `max_tokens` | 1200 | ≈ 9–10 s of audio. Raise for longer utterances. |
|
||||
| `seed` | `None` | Pass an int for reproducible output across runs. |
|
||||
|
||||
---
|
||||
|
||||
## Token format
|
||||
|
||||
The tokenizer is Llama-3's, with Orpheus's audio-codebook special tokens
|
||||
laid out above the standard text vocabulary:
|
||||
|
||||
| Token | ID | Purpose |
|
||||
|---|---|---|
|
||||
| `<\|begin_of_text\|>` | 128000 | Llama-3 BOS (auto-prepended by tokenizer) |
|
||||
| `<\|end_of_text\|>` | 128009 | end of human turn (text portion) |
|
||||
| `START_OF_SPEECH` | 128257 | model emits this just before audio codes |
|
||||
| `END_OF_SPEECH` | 128258 | model emits this when it finishes — used as `eos_token_id` / `stop_token_ids` |
|
||||
| `START_OF_HUMAN` | 128259 | wrap the text prompt |
|
||||
| `END_OF_HUMAN` | 128260 | wrap the text prompt |
|
||||
| `START_OF_AI` | 128261 | model emits this to begin its response |
|
||||
| `END_OF_AI` | 128262 | model emits this when fully done |
|
||||
| `PAD_TOKEN` | 128263 | left-padding for batched generation |
|
||||
| audio codebook | 128266 + N·4096 | SNAC codes, N ∈ {0..6} for 7-frame layout |
|
||||
|
||||
**Training prompt structure** (and what the model expects at inference):
|
||||
|
||||
```
|
||||
[SOH] + tokenize("salt_<lang>_<NNNN>: <text>") + [EOT] + [EOH]
|
||||
↳ model autoregressively emits:
|
||||
[SOA] + [SOS] + audio_codes... + [EOS] + [EOA]
|
||||
```
|
||||
|
||||
To recover audio: find the **last** `START_OF_SPEECH` (128257) in the
|
||||
output, take everything after it, drop any token outside the audio
|
||||
codebook range, group into 7-token frames, undo the per-position offsets,
|
||||
and feed the three layers to `SNAC.decode`. Both inference snippets above
|
||||
implement this end-to-end.
|
||||
|
||||
---
|
||||
|
||||
## Training details
|
||||
|
||||
| Setting | Value |
|
||||
|---|---|
|
||||
| Base model | `unsloth/orpheus-3b-0.1-pretrained` (raw pretrained, not the `-ft` voice-actor variant) |
|
||||
| Adapter | LoRA r=64, α=64, dropout=0, bias=none |
|
||||
| Target modules | `q_proj`, `k_proj`, `v_proj`, `o_proj`, `gate_proj`, `up_proj`, `down_proj` |
|
||||
| Optimizer | `adamw_8bit`, weight decay 0.001 |
|
||||
| LR schedule | linear, lr=2e-4, warmup steps=5 |
|
||||
| Per-device batch size | 1 (with `gradient_accumulation_steps=4`, effective batch = 4) |
|
||||
| Epochs | 3 |
|
||||
| `max_seq_length` | 4096 |
|
||||
| `save_total_limit` | 2 |
|
||||
| Precision | bfloat16 weights, 16-bit LoRA |
|
||||
| Seed | 3407 |
|
||||
| Hardware | single NVIDIA RTX 4090 (24 GB) |
|
||||
| Gradient checkpointing | Unsloth's optimised variant |
|
||||
| Final save | LoRA merged into 16-bit weights via `save_pretrained_merged(save_method="merged_16bit")` |
|
||||
|
||||
The pretrained variant of Orpheus was chosen over the `-ft` voice-actor
|
||||
variant because that variant has a strong English-voice-actor prior that
|
||||
fights low-resource-language fine-tuning.
|
||||
|
||||
### Data prep summary
|
||||
|
||||
1. Load all 20 configs of `Sunbird/tts` (`get_dataset_config_names`)
|
||||
and `concatenate_datasets` their `train` and `test` splits into one
|
||||
training set and one held-out evaluation set. **No** speaker filter.
|
||||
2. Tag each row with `source = example["speaker_id"]` (per-row, not
|
||||
constant) — the model learns the multi-speaker prompt format
|
||||
`f"{speaker_id}: {text}"` across every speaker it sees.
|
||||
3. Cast `audio` to 24 kHz via `Audio(sampling_rate=24000)`.
|
||||
4. Drop rows whose tokenised text alone exceeds `max_seq_length` —
|
||||
saves expensive SNAC encoding on rows that would be filtered out
|
||||
downstream.
|
||||
5. Encode each remaining audio clip with `hubertsiuzdak/snac_24khz` →
|
||||
7 codes per frame, flattened with per-layer offsets
|
||||
`(+128266, +4096, +2·4096, …)`.
|
||||
6. Filter out rows with empty/None codes; drop consecutive duplicate
|
||||
frames.
|
||||
7. Build `input_ids = [SOH] + text_ids + [EOT] + [EOH] + [SOA] + [SOS] + audio_codes + [EOS] + [EOA]`.
|
||||
8. Drop rows whose total tokenised length exceeds `max_seq_length`
|
||||
(safety net for rows where text fits but text + audio together
|
||||
overflow the budget).
|
||||
|
||||
---
|
||||
|
||||
## Evaluation
|
||||
|
||||
Quality was evaluated qualitatively on a diverse held-out test sample:
|
||||
during training, up to 10 utterances are pulled from
|
||||
`ds_test.shuffle(seed=42)` covering as many distinct speaker_ids as
|
||||
possible. Generated audio is saved next to the ground-truth recording
|
||||
under `inference_samples/sample_<idx>_<speaker_id>.wav` so each language
|
||||
/ voice combination can be auditioned individually.
|
||||
|
||||
We did **not** run automated metrics (WER on a downstream STT, MOS
|
||||
prediction, language-confusion eval, etc.) for this release. Numbers
|
||||
will be added if/when those become part of the evaluation pipeline.
|
||||
|
||||
**Important caveat — quality varies by language.** The training corpus
|
||||
is unbalanced across the 20 configs; languages with more speaker hours
|
||||
in `Sunbird/tts` get more training signal and produce more natural
|
||||
speech. Audition the per-language samples before relying on a specific
|
||||
voice for production traffic.
|
||||
|
||||
---
|
||||
|
||||
## Intended uses & out-of-scope
|
||||
|
||||
**Intended:**
|
||||
|
||||
- Multilingual voice synthesis for accessibility, language learning,
|
||||
human–computer interaction, audio content creation, and downstream
|
||||
speech research on the 20 covered languages.
|
||||
- A reference checkpoint for the Sunbird/tts → Orpheus-3B multilingual
|
||||
fine-tuning pipeline; reproducible training recipe in
|
||||
[`Orpheus_3B_Sunbird_Multilingual.ipynb`](https://github.com/SunbirdAI/Qwen3-TTS/blob/main/orpheus-3B/Orpheus_3B_Sunbird_Multilingual.ipynb).
|
||||
|
||||
**Out of scope:**
|
||||
|
||||
- **Voice impersonation / deception.** The model imitates the timbres
|
||||
of consenting Sunbird voice donors. Do not use the generated audio
|
||||
to impersonate identifiable real persons or to produce content that
|
||||
could mislead listeners about who is speaking.
|
||||
- **High-stakes decisions.** Generated speech may contain pronunciation
|
||||
errors, prosodic artefacts, or hallucinated phrases — do not deploy
|
||||
in safety-critical contexts (medical, legal, emergency) without
|
||||
human review.
|
||||
- **Languages outside the 20 configs.** The model has no signal for
|
||||
languages not present in `Sunbird/tts`; sending German text to any
|
||||
speaker will produce garbled output, not "German with a Luganda
|
||||
accent".
|
||||
- **Code-switching.** Each speaker_id was recorded in a single language;
|
||||
the model has not seen mixed-language utterances and will likely
|
||||
produce phonetic artefacts at language boundaries within one prompt.
|
||||
- **Cross-language voice transfer.** Sending Acholi text to
|
||||
`salt_lug_0001` (a Luganda speaker_id) is undefined behaviour. The
|
||||
model has no language-conditioning input separate from the speaker
|
||||
tag, so language identity travels via the speaker_id. Use a speaker
|
||||
whose `salt_<lang>_NNNN` prefix matches the language of your text.
|
||||
|
||||
---
|
||||
|
||||
## Limitations & risks
|
||||
|
||||
- **Quality varies by language.** Per-language data volume in
|
||||
`Sunbird/tts` is unbalanced. Languages with fewer hours produce
|
||||
noticeably less natural speech. Run the per-language test-split
|
||||
audit (script below) before committing to a particular voice.
|
||||
- **No language conditioning.** There is no `language` token; the
|
||||
model relies entirely on the speaker_id to disambiguate. Mismatching
|
||||
speaker_id and text language is undefined behaviour (see above).
|
||||
- **Vocabulary coverage.** Limited to the lexicon present in each
|
||||
config's training subset. Unfamiliar words, code-switching, and
|
||||
out-of-distribution proper nouns may produce artefacts.
|
||||
- **Long utterances.** The model was trained on utterances up to ~16 s
|
||||
of audio (`max_seq_length=4096`). Generation may degrade or
|
||||
truncate beyond ~10 s of speech.
|
||||
- **Sampling variance.** With `do_sample=True`, identical prompts can
|
||||
produce noticeably different deliveries between runs. Pass `seed=`
|
||||
for reproducibility.
|
||||
- **No emotion/style control.** Unlike the upstream `orpheus-3b-0.1-ft`,
|
||||
this fine-tune was not exposed to in-text emotion tags
|
||||
(`<laugh>`, `<sigh>`, …). Such tags will be tokenised as ordinary
|
||||
text and produce no special prosodic effect.
|
||||
- **Bias.** Inherits any biases present in the Sunbird/tts corpus and
|
||||
in Llama-3's pretraining; we have not audited these systematically
|
||||
per language.
|
||||
|
||||
### Quick per-language audit script
|
||||
|
||||
```python
|
||||
from datasets import load_dataset, Audio, get_dataset_config_names
|
||||
import soundfile as sf
|
||||
from pathlib import Path
|
||||
|
||||
CONFIGS = get_dataset_config_names("Sunbird/tts")
|
||||
out_dir = Path("language_audit"); out_dir.mkdir(exist_ok=True)
|
||||
|
||||
for cfg in CONFIGS:
|
||||
ds = load_dataset("Sunbird/tts", cfg, split="test")
|
||||
ds = ds.cast_column("audio", Audio(sampling_rate=24000))
|
||||
row = ds[0]
|
||||
sid, text = row["speaker_id"], row["text"]
|
||||
print(f"{cfg}: {sid} -> {text[:80]}")
|
||||
wav = synthesize(text, speaker_id=sid, seed=0)
|
||||
sf.write(out_dir / f"{cfg}_{sid}.wav", wav, 24000)
|
||||
sf.write(out_dir / f"{cfg}_{sid}_groundtruth.wav",
|
||||
row["audio"]["array"], 24000)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hardware requirements
|
||||
|
||||
| Mode | Min VRAM | Recommended |
|
||||
|---|---|---|
|
||||
| `transformers` + Unsloth, fp16 | 8 GB (with `load_in_4bit=True`) | 16 GB |
|
||||
| `transformers` + Unsloth, bf16 | 14 GB | 24 GB |
|
||||
| vLLM, bf16, `max_model_len=4096` | 14 GB | 24 GB |
|
||||
|
||||
Audio decoding via SNAC runs on CPU and adds ~50–150 ms per utterance.
|
||||
|
||||
---
|
||||
|
||||
## License & attribution
|
||||
|
||||
This fine-tune is released under **Apache-2.0**, matching the upstream
|
||||
[`unsloth/orpheus-3b-0.1-pretrained`](https://huggingface.co/unsloth/orpheus-3b-0.1-pretrained)
|
||||
license. It transitively inherits obligations from:
|
||||
|
||||
- The [Orpheus-TTS](https://github.com/canopyai/Orpheus-TTS) project (CanopyAI).
|
||||
- The [Llama-3](https://llama.meta.com/llama3/) base architecture and weights — Meta Llama 3 Community License.
|
||||
- The [SNAC](https://github.com/hubertsiuzdak/snac) audio codec (Hubert Siuzdak, MIT).
|
||||
- The [`Sunbird/tts`](https://huggingface.co/datasets/Sunbird/tts) dataset and the SALT voice donors who contributed recordings.
|
||||
|
||||
If you redistribute the merged weights, please carry these attributions
|
||||
forward.
|
||||
|
||||
---
|
||||
|
||||
## Citation
|
||||
|
||||
If you use this model in your work, please cite both the dataset and the
|
||||
fine-tuning project:
|
||||
|
||||
```bibtex
|
||||
@misc{sunbird_orpheus3b_multilingual_2026,
|
||||
title = {Orpheus-3B Sunbird Multilingual TTS},
|
||||
author = {Sunbird AI},
|
||||
year = {2026},
|
||||
howpublished = {\url{https://huggingface.co/sunbird/orpheus-3b-tts-multilingual}},
|
||||
}
|
||||
|
||||
@misc{sunbird_tts_dataset,
|
||||
title = {Sunbird Speech Dataset},
|
||||
author = {Sunbird AI},
|
||||
howpublished = {\url{https://huggingface.co/datasets/Sunbird/tts}},
|
||||
}
|
||||
|
||||
@misc{orpheus_tts_2025,
|
||||
title = {Orpheus-TTS},
|
||||
author = {Canopy Labs},
|
||||
year = {2025},
|
||||
howpublished = {\url{https://github.com/canopyai/Orpheus-TTS}},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Single-speaker variant
|
||||
|
||||
If you only need one specific voice and want a smaller, more focused
|
||||
checkpoint, see
|
||||
[`sunbird/orpheus-3b-tts-salt-lug-0001`](https://huggingface.co/sunbird/orpheus-3b-tts-salt-lug-0001)
|
||||
— same recipe, scoped to a single Luganda speaker.
|
||||
Reference in New Issue
Block a user