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
98 lines
3.8 KiB
Python
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)
|