Files
2026-07-13 13:25:10 +08:00

70 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""Export Paraformer (SANM encoder + CIF predictor + SANM decoder) to GGUF.
Encoder reuses the shared SAN-M forward. Predictor (CIF) runs on host in C++.
"""
import argparse, os, re
import numpy as np, torch, gguf
def parse_mvn(path):
# am.mvn (kaldi nnet) has 3 bracketed blocks: [Splice idx], [AddShift=shift],
# [Rescale=scale]. Take the two 560-length vectors (shift then scale).
# apply: out = (x + shift) * scale
blocks = [np.array([float(x) for x in b.split()], np.float32)
for b in re.findall(r"\[([^\]]*)\]", open(path).read())]
vecs = [b for b in blocks if b.size > 1]
return vecs[0], vecs[1]
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--model_pt", required=True); ap.add_argument("--mvn", required=True)
ap.add_argument("--out", required=True); ap.add_argument("--wtype", default="f32", choices=["f32","f16","q8_0"])
ap.add_argument("--tokens", default=None, help="tokens.json (id->token); default: next to model_pt")
a = ap.parse_args()
sd = torch.load(a.model_pt, map_location="cpu"); sd = sd.get("state_dict", sd)
w = gguf.GGUFWriter(a.out, "paraformer")
w.add_uint32("pf.enc.output_size", 512); w.add_uint32("pf.enc.attention_heads", 4)
w.add_uint32("pf.enc.num_blocks", 50); w.add_uint32("pf.enc.kernel_size", 11)
w.add_uint32("pf.dec.num_blocks", 16); w.add_uint32("pf.dec.att_layer_num", 16)
w.add_uint32("pf.dec.decoders3", 1); w.add_uint32("pf.dec.attention_heads", 4)
w.add_uint32("pf.dec.kernel_size", 11); w.add_uint32("pf.vocab_size", 8404)
import json, glob
tp = a.tokens or (glob.glob(os.path.join(os.path.dirname(a.model_pt), "tokens.json")) + [None])[0]
if tp and os.path.exists(tp):
with open(tp, encoding="utf-8") as f: toks = json.load(f)
w.add_array("pf.vocab", toks)
print(f"embedded pf.vocab ({len(toks)} tokens) from {tp}")
else:
print("WARNING: tokens.json not found - gguf will have no vocab (binary falls back to ids)")
w.add_float32("pf.predictor.tail_threshold", 0.45)
w.add_float32("pf.predictor.threshold", 1.0)
shift, scale = parse_mvn(a.mvn)
w.add_tensor("cmvn.shift", shift); w.add_tensor("cmvn.scale", scale)
n = 0
for k, v in sd.items():
if not (k.startswith("encoder.") or k.startswith("decoder.") or k.startswith("predictor.")):
continue
if k == "decoder.embed.0.weight": # token embedding, unused at NAR inference
continue
arr = v.detach().to(torch.float32).contiguous().numpy()
if k.endswith("fsmn_block.weight") and arr.ndim == 3: # (D,1,K)->(K,D)
arr = np.ascontiguousarray(arr[:, 0, :].T)
elif args_f16(a) and arr.ndim == 2 and "norm" not in k and "cif_output" not in k:
arr = arr.astype(np.float16)
if a.wtype == "q8_0" and arr.ndim == 2 and "norm" not in k and "fsmn_block" not in k and "predictor" not in k and arr.shape[1] % 32 == 0:
from gguf import quants as _q, GGMLQuantizationType as _QT
w.add_tensor(k, _q.quantize(arr, _QT.Q8_0), raw_dtype=_QT.Q8_0)
else:
w.add_tensor(k, arr)
n += 1
print(f"writing {n} tensors (+cmvn) to {a.out}")
w.write_header_to_file(); w.write_kv_data_to_file(); w.write_tensors_to_file(); w.close()
print(f"done: {a.out} ({os.path.getsize(a.out)/1e6:.1f} MB)")
def args_f16(a): return a.wtype == "f16"
if __name__ == "__main__":
main()