Files
sherpa-onnx/python-api-examples/test-offline-tts-zipvoice-phonemize.py
Fangjun Kuang 0b3647632f Remove espeak-ng and piper-phonemize from sherpa-onnx (#4055)
Fixes #3731

espeak-ng and piper-phonemize are no longer built into sherpa-onnx.

Text frontends are now:

- **lexicon** — built in, no extra dependency
- **phonemize** — phonemize outside sherpa-onnx (e.g. [piper-phonemize](https://github.com/csukuangfj/piper-phonemize)) and pass the result in. That library is GPL, so it stays optional: sherpa-onnx is GPL-free after this PR.

## API

- `SherpaOnnxGenerationConfig` gains `phoneme_codepoints` and `tokens` (mutually exclusive alternatives to `text`)
- `data_dir` on the TTS model configs is deprecated and ignored; the ABI slot is kept
- all bindings updated: C/C++, Python, Java, Kotlin, Go, Rust, C#, Swift, Pascal, Dart/Flutter, node-addon, WASM, HarmonyOS
2026-10-20 11:09:04 +08:00

182 lines
6.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Test ZipVoice Chinese+English TTS model.
If you want to use a lexicon instead, please see
test-offline-tts-zipvoice-lexicon.py.
Chinese text uses pypinyin for phonemization (initial0 final_tone format,
e.g., "好" -> "h0 ao3").
English text uses piper_phonemize (espeak-ng) for IPA phonemization.
Supported extra parameters in gen_config.extra (all optional):
- min_words_in_sentence, int, default 5.
Merge adjacent sentences if the number of words is less than this.
- max_words_in_sentence, int, default 20.
Split a sentence into chunks if the number of words exceeds this.
- num_steps, int, default 4.
Number of flow-matching denoising steps.
- debug, int, default from model config.
Install dependencies:
pip install pypinyin
pip install piper_phonemize -f https://k2-fsa.github.io/icefall/piper_phonemize.html
Download model:
wget https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/sherpa-onnx-zipvoice-distill-int8-zh-en-emilia.tar.bz2
tar xf sherpa-onnx-zipvoice-distill-int8-zh-en-emilia.tar.bz2
wget https://github.com/k2-fsa/sherpa-onnx/releases/download/vocoder-models/vocos_24khz.onnx
"""
import re
import time
import numpy as np
import sherpa_onnx
import soundfile as sf
try:
from pypinyin import Style, lazy_pinyin
from pypinyin.contrib.tone_convert import to_finals_tone3, to_initials
except Exception as ex:
raise RuntimeError(f"{ex}\nPlease run\npip install pypinyin")
try:
from piper_phonemize import phonemize_espeak
except Exception as ex:
raise RuntimeError(
f"{ex}\nPlease run\n"
"pip install piper_phonemize -f https://k2-fsa.github.io/icefall/piper_phonemize.html"
)
def get_initial_final(token):
"""Convert a pinyin token to ZipVoice format: initial0 final_tone.
Example: "hao3" -> "h0 ao3", "de" -> "d0 e5"
"""
initial = to_initials(token, strict=False)
final = to_finals_tone3(token, strict=False, neutral_tone_with_five=True)
ans = ""
if initial:
ans = initial + "0"
if final:
ans += f" {final}"
return ans
def tokenize_mixed(text, lang):
"""Tokenize mixed Chinese+English text for ZipVoice.
Chinese: pypinyin -> initial0 final_tone format
English: piper_phonemize -> IPA phonemes
"""
# Replace Chinese punctuations with ASCII equivalents
for zh, en in [(",", ","), ("。", "."), ("!", "!"), ("?", "?"),
(";", ";"), (":", ":"), ("、", ",")]:
text = text.replace(zh, en)
# Split into: Chinese chars | punctuation | English/ASCII text
segments = re.findall(
r"[一-鿿]+"
r"|[,.\!?;:]"
r"|[a-zA-Z0-9 ]+",
text,
)
all_tokens = []
for seg in segments:
if re.match(r"[一-鿿]+", seg):
# Chinese segment: convert each character to initial0 final
for ch in seg:
py = lazy_pinyin(ch, style=Style.TONE3,
tone_sandhi=True,
neutral_tone_with_five=True)
if py:
initial_final = get_initial_final(py[0])
for token in initial_final.split():
all_tokens.append(token)
elif re.match(r"[,.\!?;:]", seg):
# Punctuation: add directly as ASCII token
all_tokens.append(seg)
else:
# English/ASCII text: use piper_phonemize
seg = seg.strip()
if not seg:
continue
sentences = phonemize_espeak(seg, lang)
for sentence in sentences:
for c in sentence:
all_tokens.append(c)
return [all_tokens]
model_dir = "./sherpa-onnx-zipvoice-distill-int8-zh-en-emilia"
tts_config = sherpa_onnx.OfflineTtsConfig(
model=sherpa_onnx.OfflineTtsModelConfig(
zipvoice=sherpa_onnx.OfflineTtsZipvoiceModelConfig(
tokens=f"{model_dir}/tokens.txt",
encoder=f"{model_dir}/encoder.int8.onnx",
decoder=f"{model_dir}/decoder.int8.onnx",
vocoder="./vocos_24khz.onnx",
),
debug=True,
),
)
tts = sherpa_onnx.OfflineTts(tts_config)
# Load reference audio
ref_wav = f"{model_dir}/test_wavs/news-female.wav"
ref_audio, ref_sr = sf.read(ref_wav)
ref_audio = ref_audio.astype(np.float32)
ref_text = "各位村民, 大家新年好! 近期, 湖北省武汉市等多个地区"
text = "在这个快速发展的时代,人工智能artificial intelligence技术正在改变我们的生活方式。语音合成speech synthesis作为重要应用之一,让机器能够用自然流畅的语音与人类进行交流。"
# Tokenize reference text and generated text separately
# The espeak-ng voice for English segments is taken from the model metadata.
# Fall back to en-us if the model has no language information.
lang = tts.lang
if not lang:
print("Warning: the model has no language information. Falling back to en-us.")
lang = "en-us"
print("Phonemizer language:", lang)
ref_tokens = tokenize_mixed(ref_text, lang)
gen_tokens = tokenize_mixed(text, lang)
gen_config = sherpa_onnx.GenerationConfig()
gen_config.sid = 0
gen_config.speed = 1.0
# tokens[0] = reference text tokens, tokens[1..] = generated text tokens
gen_config.tokens = ref_tokens + gen_tokens
gen_config.reference_audio = ref_audio.tolist()
gen_config.reference_sample_rate = ref_sr
gen_config.reference_text = ref_text
print(f"Reference text: {ref_text}")
print(f"Synthesis text: {text}")
start = time.time()
audio = tts.generate("", gen_config)
end = time.time()
assert len(audio.samples) > 0, "No audio generated!"
assert audio.sample_rate > 0, "Invalid sample rate!"
filename = "test-zipvoice-phonemize.wav"
sf.write(filename, audio.samples, samplerate=audio.sample_rate)
elapsed_seconds = end - start
audio_duration = len(audio.samples) / audio.sample_rate
rtf = elapsed_seconds / audio_duration
print(f"Saved to {filename}")
print(f"Elapsed seconds: {elapsed_seconds:.3f}")
print(f"Audio duration in seconds: {audio_duration:.3f}")
print(f"RTF: {elapsed_seconds:.3f}/{audio_duration:.3f} = {rtf:.3f}")