feat: add expressive offline Russian book TTS
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prepare a Piper ONNX export for sherpa-onnx mobile inference."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import onnx
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--model", type=Path, required=True)
|
||||
parser.add_argument("--config", type=Path, required=True)
|
||||
parser.add_argument("--output-dir", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
config = json.loads(args.config.read_text(encoding="utf-8"))
|
||||
output_dir: Path = args.output_dir
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_model = output_dir / "aletheia_ru.onnx"
|
||||
output_config = output_dir / "aletheia_ru.onnx.json"
|
||||
output_tokens = output_dir / "tokens.txt"
|
||||
|
||||
shutil.copy2(args.model, output_model)
|
||||
shutil.copy2(args.config, output_config)
|
||||
|
||||
id_map = config["phoneme_id_map"]
|
||||
# sherpa-onnx's Piper lexicon maps one Unicode code point to one model ID.
|
||||
# Newer Piper configs may also contain English diphthong aliases such as
|
||||
# "aɪ". eSpeak emits their component code points, and sherpa rejects the
|
||||
# multi-code-point aliases, so only the single-code-point table is mobile-safe.
|
||||
token_rows = sorted(
|
||||
((ids[0], symbol) for symbol, ids in id_map.items() if len(symbol) == 1),
|
||||
key=lambda row: row[0],
|
||||
)
|
||||
actual_ids = [row[0] for row in token_rows]
|
||||
if len(actual_ids) != len(set(actual_ids)):
|
||||
raise RuntimeError("phoneme_id_map contains duplicate token identifiers")
|
||||
if not actual_ids or min(actual_ids) < 0 or max(actual_ids) >= config["num_symbols"]:
|
||||
raise RuntimeError("phoneme_id_map contains a token outside the model symbol range")
|
||||
output_tokens.write_text(
|
||||
"".join(f"{symbol} {token_id}\n" for token_id, symbol in token_rows),
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
|
||||
model = onnx.load(str(output_model))
|
||||
metadata = {
|
||||
"model_type": "vits",
|
||||
"comment": "piper",
|
||||
"language": "Russian",
|
||||
"voice": config["espeak"]["voice"],
|
||||
"has_espeak": "1",
|
||||
"n_speakers": str(config["num_speakers"]),
|
||||
"sample_rate": str(config["audio"]["sample_rate"]),
|
||||
}
|
||||
existing = {item.key: item for item in model.metadata_props}
|
||||
for key, value in metadata.items():
|
||||
if key in existing:
|
||||
existing[key].value = value
|
||||
else:
|
||||
item = model.metadata_props.add()
|
||||
item.key = key
|
||||
item.value = value
|
||||
onnx.save(model, str(output_model))
|
||||
|
||||
print(json.dumps({
|
||||
"model": str(output_model),
|
||||
"model_bytes": output_model.stat().st_size,
|
||||
"tokens": len(token_rows),
|
||||
"metadata": metadata,
|
||||
}, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user