voice_audio_multi/tools/community_models/convert_moonshine_asr.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

107 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""Convert moonshine-ai/moonshine streaming safetensors checkpoints to audio.cpp GGUF packages."""
import argparse
import json
import shutil
import subprocess
import struct
import tempfile
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
SPEC = REPO_ROOT / "model_specs" / "moonshine_asr.json"
SCALAR_TENSORS = {"model.encoder.embedder.comp.log_k"}
def require_v1_spec(path: Path) -> None:
with path.open("r", encoding="utf-8") as handle:
spec = json.load(handle)
if spec.get("schema_version") != 1:
raise SystemExit(f"{path} must be a schema_version 1 model spec")
def rank1_scalar_safetensors(source: Path, destination: Path) -> None:
"""Copy safetensors while exposing known rank-0 scalars as length-1 tensors."""
with source.open("rb") as f:
header_len = struct.unpack("<Q", f.read(8))[0]
header = json.loads(f.read(header_len))
payload = f.read()
changed = False
for name in SCALAR_TENSORS:
entry = header.get(name)
if entry is not None and entry.get("shape") == []:
entry["shape"] = [1]
changed = True
if not changed:
shutil.copyfile(source, destination)
return
encoded = json.dumps(header, separators=(",", ":")).encode("utf-8")
padding = (8 - (len(encoded) % 8)) % 8
destination.write_bytes(struct.pack("<Q", len(encoded) + padding) + encoded + (b" " * padding) + payload)
def convert(converter: Path, checkpoint: Path, output: Path, quant_type: str, overwrite: bool) -> None:
require_v1_spec(SPEC)
ckpt = checkpoint / "model.safetensors"
if not ckpt.exists():
candidates = sorted(checkpoint.glob("*.safetensors"))
if not candidates:
raise SystemExit(f"No .safetensors checkpoint found in {checkpoint}")
ckpt = candidates[-1]
output.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix="moonshine-asr-convert-") as tmp_dir:
converted_input = Path(tmp_dir) / ckpt.name
rank1_scalar_safetensors(ckpt, converted_input)
command = [
str(converter),
"--input",
str(converted_input),
"--root",
str(checkpoint),
"--family",
"moonshine_asr",
"--model-spec",
str(SPEC),
"--type",
quant_type,
"--output",
str(output),
]
if overwrite:
command.append("--overwrite")
print("+", " ".join(command))
subprocess.run(command, check=True)
for name in ["config.json", "tokenizer.json"]:
source = checkpoint / name
if source.exists():
shutil.copyfile(source, output.parent / name)
print(f"copied {name} -> {output.parent}")
def main() -> None:
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--checkpoint", type=Path, required=True, help="HF checkpoint directory")
parser.add_argument("--converter", type=Path, required=True, help="path to audiocpp_gguf")
parser.add_argument("--output", type=Path, required=True, help="output .gguf path")
parser.add_argument(
"--type",
default="q8_0",
choices=["orig", "f32", "f16", "bf16", "q8_0", "q4_k", "q5_k", "q6_k"],
help="GGUF tensor storage type",
)
parser.add_argument("--overwrite", action="store_true", help="overwrite output")
args = parser.parse_args()
convert(args.converter, args.checkpoint, args.output, args.type, args.overwrite)
if __name__ == "__main__":
main()