voice_audio_multi/tools/models/convert_ming3_tts_gguf.py
Fabio c36510c21e
Some checks failed
Linux Build / Linux cpu (push) Has been cancelled
Linux Build / Linux cpu-clang (push) Has been cancelled
Linux Build / Linux vulkan (push) Has been cancelled
mac-build / macOS CPU (push) Has been cancelled
Nix Build / Nix (cpu) (push) Has been cancelled
Nix Build / Nix (vulkan) (push) Has been cancelled
windows-build / Windows CPU (push) Has been cancelled
Initial import: audio.cpp with Orange Pi documentation
2026-11-04 19:05:22 +01:00

86 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""Package Ming's model, tokenizer and native CAMPPlus speaker weights in one GGUF."""
import argparse
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
import numpy as np
import onnx
from onnx import numpy_helper
from safetensors.numpy import save_file
def campplus_weights(path):
model = onnx.load(path)
constants = {item.name: numpy_helper.to_array(item) for item in model.graph.initializer}
for node in model.graph.node:
if node.op_type == "Identity" and node.input[0] in constants:
constants[node.output[0]] = constants[node.input[0]]
elif node.op_type == "Constant":
for attr in node.attribute:
if attr.name == "value":
constants[node.output[0]] = numpy_helper.to_array(attr.t)
weights = {}
for node in model.graph.node:
if node.op_type not in ("Conv", "BatchNormalization"):
continue
name = re.sub(r"/(layer\d+)/\1\.(\d+)", r"/\1/\2", node.name)
name = re.sub(r"/shortcut/shortcut\.(\d+)", r"/shortcut/\1", name)
prefix = "speaker_encoder." + name.rsplit("/", 1)[0].strip("/").replace("/", ".")
suffixes = ("weight", "bias") if node.op_type == "Conv" else (
"weight", "bias", "running_mean", "running_var")
if len(node.input) not in ((2, 3) if node.op_type == "Conv" else (5,)):
raise ValueError(f"Unsupported parameter count: {node.name}")
for source, suffix in zip(node.input[1:], suffixes):
value = constants[source]
if value.dtype != np.float32:
raise ValueError(f"CAMPPlus parameter {source} must be float32, got {value.dtype}")
target = prefix + "." + suffix
if target in weights:
raise ValueError(f"Duplicate CAMPPlus parameter: {target}")
weights[target] = np.ascontiguousarray(value)
if node.op_type == "BatchNormalization":
attrs = {attr.name: onnx.helper.get_attribute_value(attr) for attr in node.attribute}
if abs(attrs.get("epsilon", 1e-5) - 1e-5) > 1e-10 or attrs.get("training_mode", 0):
raise ValueError(f"Unsupported CAMPPlus batch normalization: {node.name}")
if weights["speaker_encoder.xvector.dense.linear.weight"].shape != (192, 1024, 1):
raise ValueError("Ming requires the published 192-dimensional CAMPPlus encoder")
return weights
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source", type=Path, required=True, help="Published Ming checkpoint directory")
parser.add_argument("--output", type=Path, required=True, help="Output GGUF file")
parser.add_argument("--type", choices=("orig", "q8_0"), default="orig")
parser.add_argument("--converter", type=Path, default=Path("build/debug/bin/audiocpp_gguf"))
parser.add_argument("--overwrite", action="store_true")
args = parser.parse_args()
if args.output.exists() and not args.overwrite:
raise FileExistsError(args.output)
source = args.source.resolve()
weights = campplus_weights(source / "campplus.onnx")
args.output.parent.mkdir(parents=True, exist_ok=True)
spec = Path(__file__).resolve().parents[2] / "model_specs/ming3_tts.json"
with tempfile.TemporaryDirectory(prefix=".ming-convert-", dir=args.output.parent) as temporary:
root = Path(temporary)
for name in ("config.json", "tokenizer.json", "tokenizer_config.json"):
shutil.copyfile(source / name, root / name)
speaker = root / "speaker.safetensors"
save_file(weights, speaker)
command = [str(args.converter.resolve()), "--input", str(source / "model.safetensors"),
"--input", f"speaker={speaker}", "--root", str(root), "--model-spec", str(spec),
"--type", args.type, "--output", str(args.output.resolve()),
"--keep-type", "speaker.*=f32", "--keep-type", "stop_head*=bf16"]
if args.overwrite:
command.append("--overwrite")
subprocess.run(command, check=True)
if __name__ == "__main__":
main()