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
124 lines
5.3 KiB
Python
124 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Convert a sherpa-onnx streaming Zipformer keyword spotter to GGUF."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import importlib.util
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import onnx
|
|
from safetensors.numpy import save_file
|
|
|
|
|
|
REPO = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
def load_zipformer_converter():
|
|
path = REPO / "tools/community_models/convert_kroko_onnx.py"
|
|
spec = importlib.util.spec_from_file_location("audiocpp_zipformer_converter", path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
assert spec.loader is not None
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def metadata(model_path: Path, converter) -> tuple[dict[str, object], list[str]]:
|
|
model = onnx.load(model_path, load_external_data=False)
|
|
values = {item.key: item.value for item in model.metadata_props}
|
|
|
|
def ints(key: str) -> list[int]:
|
|
return [int(part) for part in values[key].split(",")]
|
|
|
|
layers = ints("num_encoder_layers")
|
|
prefixes: list[str] = []
|
|
for stack, count in enumerate(layers):
|
|
base = f"encoder.encoders.{stack}"
|
|
if stack != 0:
|
|
base += ".encoder"
|
|
prefixes.extend(f"{base}.layers.{layer}" for layer in range(count))
|
|
left = ints("left_context_len")
|
|
base_left = left[0]
|
|
if any(value <= 0 or base_left % value != 0 for value in left):
|
|
raise ValueError("invalid Zipformer left-context metadata")
|
|
config = {
|
|
"audiocpp_family": "sherpa_kws",
|
|
"model_type": values.get("model_type", "zipformer2"),
|
|
"variant": "sherpa-onnx-streaming-kws",
|
|
"sample_rate": 16000,
|
|
"feature_dim": 80,
|
|
"chunk_size": int(values["T"]),
|
|
"chunk_shift": int(values["decode_chunk_len"]),
|
|
"subsampling_factor": 4,
|
|
"encoder_dims": ints("encoder_dims"),
|
|
"query_head_dims": ints("query_head_dims"),
|
|
"value_head_dims": ints("value_head_dims"),
|
|
"num_heads": ints("num_heads"),
|
|
"num_encoder_layers": layers,
|
|
"cnn_module_kernels": ints("cnn_module_kernels"),
|
|
"left_context_len": left,
|
|
"downsampling_factors": [base_left // value for value in left],
|
|
}
|
|
return config, prefixes
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("input", type=Path, help="directory containing encoder, decoder, joiner and tokens files")
|
|
parser.add_argument("output", type=Path, help="output GGUF path")
|
|
parser.add_argument("--encoder", default="encoder-epoch-13-avg-2-chunk-16-left-64.onnx")
|
|
parser.add_argument("--decoder", default="decoder-epoch-13-avg-2-chunk-16-left-64.onnx")
|
|
parser.add_argument("--joiner", default="joiner-epoch-13-avg-2-chunk-16-left-64.onnx")
|
|
parser.add_argument("--keywords", type=Path, help="default tokenized keyword file")
|
|
parser.add_argument("--audiocpp-gguf", type=Path, default=REPO / "build/debug/bin/audiocpp_gguf")
|
|
parser.add_argument("--overwrite", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
root = args.input.resolve()
|
|
output = args.output.resolve()
|
|
paths = {name: root / getattr(args, name) for name in ("encoder", "decoder", "joiner")}
|
|
paths["tokens"] = root / "tokens.txt"
|
|
missing = [str(path) for path in paths.values() if not path.is_file()]
|
|
if missing:
|
|
raise SystemExit("missing sherpa resources: " + ", ".join(missing))
|
|
if output.exists() and not args.overwrite:
|
|
raise SystemExit(f"output exists (pass --overwrite): {output}")
|
|
|
|
converter = load_zipformer_converter()
|
|
config, prefixes = metadata(paths["encoder"], converter)
|
|
converter.LAYER_PREFIXES = prefixes
|
|
tensors = converter.convert_graph(paths["encoder"], "encoder", linear_pos_prefixes=prefixes)
|
|
tensors.update(converter.convert_graph(paths["decoder"], "decoder"))
|
|
tensors.update(converter.convert_graph(paths["joiner"], "joiner"))
|
|
decoder_meta = converter.decoder_metadata(paths["decoder"])
|
|
config.update(decoder_meta)
|
|
|
|
keywords = args.keywords.resolve() if args.keywords else root / "test_wavs/keywords.txt"
|
|
if not keywords.is_file():
|
|
raise SystemExit(f"default tokenized keyword file not found: {keywords}")
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
with tempfile.TemporaryDirectory(prefix="audiocpp-sherpa-kws-") as temporary:
|
|
staging = Path(temporary)
|
|
save_file(tensors, str(staging / "model.safetensors"), metadata={
|
|
"format": "pt", "source": "sherpa-onnx", "audiocpp_family": "sherpa_kws"})
|
|
(staging / "config.json").write_text(json.dumps(config, indent=2) + "\n")
|
|
shutil.copyfile(paths["tokens"], staging / "tokens.txt")
|
|
shutil.copyfile(keywords, staging / "keywords.txt")
|
|
command = [str(args.audiocpp_gguf.resolve()), "--input", f"weights={staging / 'model.safetensors'}",
|
|
"--output", str(output), "--type", "orig", "--family", "sherpa_kws",
|
|
"--model-spec", str(REPO / "model_specs/sherpa_kws.json"), "--root", str(staging)]
|
|
if args.overwrite:
|
|
command.append("--overwrite")
|
|
print("+", " ".join(command))
|
|
subprocess.run(command, check=True)
|
|
for name in ("config.json", "tokens.txt", "keywords.txt"):
|
|
shutil.copyfile(staging / name, output.parent / name)
|
|
print(f"wrote {output} ({len(tensors)} tensors)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|