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

98 lines
3.8 KiB
Python

"""Repackage the official Cactus Whistle checkpoint as an experimental audio.cpp GGUF.
Pin Cactus-Compute/whistle at b358ddadd89b7a713b5aa131f23032d3cca1b251.
The checkpoint supplies FP32 weights; whistle.cact supplies the custom tokenizer,
mel filterbank, and Hadamard permutations for the native whistle_asr family.
The output embeds these assets, the upstream LICENSE, and the schema-v1 spec.
"""
import argparse
import hashlib
import subprocess
from pathlib import Path
from extract_assets import extract
SOURCE = {
"whistle.safetensors": (
220618620,
"5fd58c246f522ecee2b568b306598a3befdc274c225e3a7376abce277ab3d107",
),
"config.json": (
603,
"ac2dbcb98a945eff2de98a03104c468dc9d68c06b9ff791539327f90298a3d43",
),
"LICENSE": (
11358,
"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
),
}
ARCHIVE = (
16919407,
"b6e02f048568ac5d01a2042556c658061e699acbc0aa2a1439f52f3d461dffeb",
)
ASSETS = {
"hadamard_perm1.f32",
"hadamard_perm2.f32",
"mel_filterbank_80.f32",
"tokenizer.blob",
}
SPEC = Path(__file__).resolve().parents[3] / "model_specs" / "whistle_asr.json"
def verify(path: Path, expected: tuple[int, str]) -> None:
size, sha256 = expected
if path.stat().st_size != size:
raise ValueError(f"Wrong length for {path}")
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
if digest.hexdigest() != sha256:
raise ValueError(f"SHA-256 mismatch for {path}")
def convert(source: Path, archive: Path, converter: Path, output: Path, overwrite: bool = False) -> None:
source = source.resolve()
archive = archive.resolve()
converter = converter.resolve()
output = output.resolve()
if output.is_relative_to(source):
raise ValueError("The output must not be inside the sidecar directory")
for name, expected in SOURCE.items():
verify(source / name, expected)
verify(archive, ARCHIVE)
extract(archive, source)
unexpected = {path.name for path in source.iterdir()} - set(SOURCE) - ASSETS
if unexpected:
raise ValueError(f"Unexpected source files would become GGUF sidecars: {sorted(unexpected)}")
if output.exists() and not overwrite:
raise FileExistsError(f"Pass --overwrite to replace the existing output: {output}")
output.parent.mkdir(parents=True, exist_ok=True)
command = [
str(converter), "--input", str(source / "whistle.safetensors"),
"--root", str(source), "--family", "whistle_asr", "--model-spec", str(SPEC),
"--type", "orig", "--output", str(output)
]
if overwrite:
command.append("--overwrite")
subprocess.run(command, check=True)
inspection = subprocess.run(
[str(converter), "--inspect", str(output)],
check=True, text=True, capture_output=True
).stdout.splitlines()
for field in ("tensors=116", "embedded_sidecar_count=6", "model_spec_family=whistle_asr"):
if field not in inspection:
raise RuntimeError(f"GGUF inspection is missing {field}")
print(f"Converted Whistle weights: {output}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source", required=True, type=Path, help="Directory with official safetensors, config, LICENSE")
parser.add_argument("--cact", required=True, type=Path, help="Official whistle.cact for tokenizer and frontend assets")
parser.add_argument("--converter", required=True, type=Path, help="audiocpp_gguf executable")
parser.add_argument("--output", required=True, type=Path)
parser.add_argument("--overwrite", action="store_true")
args = parser.parse_args()
convert(args.source, args.cact, args.converter, args.output, args.overwrite)