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
188 lines
6.8 KiB
Python
188 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Convert ABR Niagara batch ASR PT2 CPU packages to audio.cpp GGUF.
|
|
|
|
The upstream Niagara repositories publish Torch 2.11 PT2 archives with raw
|
|
numbered tensor payloads and a serialized graph description. This converter
|
|
stages those tensors under the graph input names, then delegates GGUF writing,
|
|
quantization, sidecar embedding, and model-spec embedding to audiocpp_gguf.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import zipfile
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
SPEC = REPO_ROOT / "model_specs" / "niagara_asr.json"
|
|
REPOS = {
|
|
"19m": {
|
|
"repo": "abr-ai/niagara-19m-batch.en",
|
|
"base": "niagara-19m-batch.en",
|
|
"tokenizer": "sentencepiece_256.model",
|
|
"output": "Niagara-ASR-GGUF/niagara-19m-batch.en-f32.gguf",
|
|
},
|
|
"38m": {
|
|
"repo": "abr-ai/niagara-38m-batch.en",
|
|
"base": "niagara-38m-batch.en",
|
|
"tokenizer": "sentencepiece_1024.model",
|
|
"output": "Niagara-ASR-GGUF/niagara-38m-batch.en-f32.gguf",
|
|
},
|
|
}
|
|
|
|
def tensor_shape(meta: dict[str, Any]) -> list[int]:
|
|
return [int(item["as_int"]) for item in meta.get("sizes", [])]
|
|
|
|
|
|
def load_raw_tensor(archive: zipfile.ZipFile, path: str, dtype_id: int, shape: list[int]) -> Any:
|
|
import torch
|
|
|
|
torch_dtype = {
|
|
4: torch.int32,
|
|
5: torch.int64,
|
|
7: torch.float32,
|
|
}
|
|
try:
|
|
dtype = torch_dtype[dtype_id]
|
|
except KeyError as error:
|
|
raise RuntimeError(f"unsupported Niagara PT2 tensor dtype id: {dtype_id}") from error
|
|
data = archive.read(path)
|
|
tensor = torch.frombuffer(bytearray(data), dtype=dtype).clone()
|
|
return tensor.reshape(shape)
|
|
|
|
|
|
def download_if_missing(model_dir: Path, variant: str) -> None:
|
|
from huggingface_hub import hf_hub_download
|
|
|
|
spec = REPOS[variant]
|
|
required = [
|
|
f"{spec['base']}-cpu.pt2",
|
|
"config.json",
|
|
"preprocessor_config.json",
|
|
"tokenizer_config.json",
|
|
spec["tokenizer"],
|
|
]
|
|
missing = [name for name in required if not (model_dir / name).exists()]
|
|
if not missing:
|
|
return
|
|
for name in missing:
|
|
print(f"downloading {spec['repo']}:{name}", flush=True)
|
|
hf_hub_download(spec["repo"], name, local_dir=model_dir)
|
|
|
|
|
|
def stage_from_pt2(model_dir: Path, variant: str, staging: Path) -> Path:
|
|
from safetensors.torch import save_file
|
|
|
|
spec = REPOS[variant]
|
|
pt2 = model_dir / f"{spec['base']}-cpu.pt2"
|
|
if not pt2.exists():
|
|
raise FileNotFoundError(f"missing Niagara CPU PT2 package: {pt2}")
|
|
|
|
with zipfile.ZipFile(pt2) as archive:
|
|
base = archive.namelist()[0].split("/")[0]
|
|
graph = json.loads(archive.read(f"{base}/models/model.json"))
|
|
constants = json.loads(archive.read(f"{base}/data/constants/model_constants_config.json"))["config"]
|
|
input_specs = graph["graph_module"]["signature"]["input_specs"]
|
|
tensor_values = graph["graph_module"]["graph"]["tensor_values"]
|
|
|
|
tensors: dict[str, Any] = {}
|
|
parameter_specs = [item["parameter"] for item in input_specs if "parameter" in item]
|
|
for index, parameter in enumerate(parameter_specs):
|
|
name = parameter["arg"]["name"]
|
|
meta = tensor_values[name]
|
|
tensors[name] = load_raw_tensor(
|
|
archive,
|
|
f"{base}/data/weights/weight_{index}",
|
|
int(meta["dtype"]),
|
|
tensor_shape(meta),
|
|
)
|
|
|
|
for item in input_specs:
|
|
if "tensor_constant" not in item:
|
|
continue
|
|
constant = item["tensor_constant"]
|
|
name = constant["arg"]["name"]
|
|
info = constants[constant["tensor_constant_name"]]
|
|
meta = info["tensor_meta"]
|
|
tensors[name] = load_raw_tensor(
|
|
archive,
|
|
f"{base}/data/constants/{info['path_name']}",
|
|
int(meta["dtype"]),
|
|
tensor_shape(meta),
|
|
)
|
|
|
|
staging.mkdir(parents=True, exist_ok=True)
|
|
weights = staging / "niagara_asr.safetensors"
|
|
save_file(tensors, weights)
|
|
shutil.copyfile(model_dir / "config.json", staging / "config.json")
|
|
shutil.copyfile(model_dir / "preprocessor_config.json", staging / "preprocessor_config.json")
|
|
shutil.copyfile(model_dir / "tokenizer_config.json", staging / "tokenizer_config.json")
|
|
shutil.copyfile(model_dir / spec["tokenizer"], staging / "sentencepiece.model")
|
|
return weights
|
|
|
|
|
|
def run_converter(converter: Path, staged_weights: Path, staging: Path, output: Path, quant_type: str, overwrite: bool) -> None:
|
|
command = [
|
|
str(converter),
|
|
"--input",
|
|
str(staged_weights),
|
|
"--root",
|
|
str(staging),
|
|
"--family",
|
|
"niagara_asr",
|
|
"--model-spec",
|
|
str(SPEC),
|
|
"--type",
|
|
quant_type,
|
|
"--output",
|
|
str(output),
|
|
]
|
|
if overwrite:
|
|
command.append("--overwrite")
|
|
print("+", " ".join(command), flush=True)
|
|
subprocess.run(command, check=True)
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--variant", choices=sorted(REPOS), required=True)
|
|
parser.add_argument("--model-dir", type=Path, help="directory containing the Niagara HF files")
|
|
parser.add_argument("--converter", type=Path, required=True, help="path to audiocpp_gguf")
|
|
parser.add_argument("--output", type=Path, help="output GGUF path")
|
|
parser.add_argument("--type", default="orig", choices=["orig", "f16", "bf16", "q8_0", "q4_k", "q5_k", "q6_k"])
|
|
parser.add_argument("--overwrite", action="store_true")
|
|
parser.add_argument("--keep-staging", type=Path, help="keep staged safetensors and sidecars in this directory")
|
|
parser.add_argument("--no-download", action="store_true", help="require all HF files to already exist locally")
|
|
args = parser.parse_args()
|
|
|
|
spec = REPOS[args.variant]
|
|
model_dir = args.model_dir or (Path("models") / spec["base"])
|
|
if not args.no_download:
|
|
download_if_missing(model_dir, args.variant)
|
|
output = args.output or Path(spec["output"])
|
|
|
|
if args.keep_staging is not None:
|
|
staging = args.keep_staging
|
|
staged_weights = stage_from_pt2(model_dir, args.variant, staging)
|
|
run_converter(args.converter, staged_weights, staging, output, args.type, args.overwrite)
|
|
return
|
|
|
|
with tempfile.TemporaryDirectory(prefix=f"niagara-{args.variant}-") as tmp:
|
|
staging = Path(tmp)
|
|
staged_weights = stage_from_pt2(model_dir, args.variant, staging)
|
|
run_converter(args.converter, staged_weights, staging, output, args.type, args.overwrite)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except Exception as error:
|
|
print(f"error: {error}", file=sys.stderr)
|
|
raise
|