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
196 lines
8.7 KiB
Python
Executable file
196 lines
8.7 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Build (and optionally publish) the VibeVoice ASR Streaming GGUF packages.
|
|
|
|
Produces the BF16/Q8_0/Q4_K GGUFs named by `model_specs/vibevoice_asr_streaming.json`
|
|
for either size, and can create the Hugging Face repo and upload them.
|
|
|
|
# convert only, into ./out
|
|
tools/models/prepare_vibevoice_asr_streaming_gguf.py --size 1.5b --out out
|
|
|
|
# convert, check each package transcribes, then publish
|
|
tools/models/prepare_vibevoice_asr_streaming_gguf.py --size 1.5b --out out \
|
|
--verify-with build/bin/audiocpp_cli \
|
|
--upload audio-cpp/VibeVoice-ASR-Streaming-1.5B-GGUF
|
|
|
|
Nothing is uploaded unless --upload is passed. Uploading needs `huggingface_hub`
|
|
and a token with write access (`hf auth login`, or HF_TOKEN in the environment).
|
|
"""
|
|
import argparse
|
|
import pathlib
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
SIZES = {
|
|
"1.5b": {"source": "microsoft/VibeVoice-ASR-Streaming-1.5B", "slug": "1.5b",
|
|
"repo": "audio-cpp/VibeVoice-ASR-Streaming-1.5B-GGUF"},
|
|
"7b": {"source": "microsoft/VibeVoice-ASR-Streaming-7B", "slug": "7b",
|
|
"repo": "audio-cpp/VibeVoice-ASR-Streaming-7B-GGUF"},
|
|
}
|
|
|
|
# Embedded verbatim so the GGUF is standalone. Everything else in the checkout --
|
|
# the README, the safetensors index -- would otherwise be swept in by `--root .`,
|
|
# since that embeds any file under 64 MB that is not itself weights.
|
|
SIDECARS = ["config.json", "tokenizer.json", "tokenizer_config.json", "vocab.json",
|
|
"merges.txt", "special_tokens_map.json", "added_tokens.json",
|
|
"preprocessor_config.json"]
|
|
|
|
FAMILY = "vibevoice_asr_streaming"
|
|
|
|
|
|
def find_converter(explicit):
|
|
if explicit:
|
|
return pathlib.Path(explicit)
|
|
root = pathlib.Path(__file__).resolve().parents[2]
|
|
found = sorted(root.glob("build*/bin/audiocpp_gguf")) + sorted(root.glob("build*/*/bin/audiocpp_gguf"))
|
|
if not found:
|
|
sys.exit("no audiocpp_gguf found under build*/; pass --gguf, or build the "
|
|
"target with: cmake --build build --target audiocpp_gguf")
|
|
return found[0]
|
|
|
|
|
|
def fetch_source(source, cache):
|
|
"""A local directory is used as-is; anything else is a repo id to snapshot."""
|
|
local = pathlib.Path(source)
|
|
if local.is_dir():
|
|
return local
|
|
try:
|
|
from huggingface_hub import snapshot_download
|
|
except ImportError:
|
|
sys.exit("huggingface_hub is needed to download the source weights: "
|
|
"pip install huggingface_hub (or pass a local --source directory)")
|
|
print(f"downloading {source} ...")
|
|
return pathlib.Path(snapshot_download(repo_id=source, cache_dir=cache))
|
|
|
|
|
|
def stage_sidecars(source, staging):
|
|
"""Copy the files that belong inside the GGUF, and nothing else."""
|
|
staging.mkdir(parents=True, exist_ok=True)
|
|
staged = []
|
|
for name in SIDECARS:
|
|
candidate = source / name
|
|
if candidate.is_file():
|
|
shutil.copy2(candidate, staging / name)
|
|
staged.append(name)
|
|
missing = {"config.json", "tokenizer.json"} - set(staged)
|
|
if missing:
|
|
sys.exit(f"source is missing required files: {', '.join(sorted(missing))}")
|
|
return staged
|
|
|
|
|
|
def convert(converter, source, staging, output, precision):
|
|
index = source / "model.safetensors.index.json"
|
|
single = source / "model.safetensors"
|
|
weights = index if index.is_file() else single
|
|
if not weights.is_file():
|
|
sys.exit(f"no safetensors weights found in {source}")
|
|
command = [str(converter), "--input", str(weights), "--output", str(output),
|
|
"--type", precision, "--family", FAMILY, "--root", str(staging), "--overwrite"]
|
|
print(" " + " ".join(command))
|
|
result = subprocess.run(command, capture_output=True, text=True)
|
|
if result.returncode != 0:
|
|
# Print the converter's own words. A conversion that fails quietly and
|
|
# leaves a truncated file behind is worse than one that stops here.
|
|
sys.stdout.write(result.stdout)
|
|
sys.stderr.write(result.stderr)
|
|
sys.exit(f"conversion to {precision} failed (exit {result.returncode})")
|
|
for line in result.stdout.splitlines():
|
|
if line.startswith(("embedded_sidecar_count=", "embedded_model_spec=", "weight_type=")):
|
|
print(" " + line)
|
|
|
|
|
|
def verify(cli, output, assets, backend):
|
|
"""Score the package rather than just checking the file exists.
|
|
|
|
⚠ The backend is explicit because it changes the answer. CPU and CUDA quantize
|
|
activations differently upstream (Q8_K/Q8_0 against Q8_1), so a quantized package
|
|
scores differently on each, and an unlabelled WER number is not interpretable.
|
|
"""
|
|
harness = pathlib.Path(__file__).resolve().parents[1] / "asr_wer.py"
|
|
command = [sys.executable, str(harness), "--cli", str(cli), "--family", FAMILY,
|
|
"--model", str(output), "--backend", backend,
|
|
"--label", f"{output.stem} [{backend}]"]
|
|
if assets:
|
|
command += ["--assets", assets]
|
|
result = subprocess.run(command, capture_output=True, text=True)
|
|
sys.stdout.write("".join(" " + line + "\n" for line in result.stdout.strip().splitlines()))
|
|
if result.returncode != 0:
|
|
sys.stderr.write(result.stderr)
|
|
sys.exit(f"verification of {output.name} failed (exit {result.returncode})")
|
|
|
|
|
|
def upload(repo, files):
|
|
try:
|
|
from huggingface_hub import HfApi
|
|
except ImportError:
|
|
sys.exit("huggingface_hub is needed to upload: pip install huggingface_hub")
|
|
api = HfApi()
|
|
print(f"creating {repo} if it does not exist ...")
|
|
api.create_repo(repo_id=repo, repo_type="model", exist_ok=True)
|
|
for path in files:
|
|
print(f"uploading {path.name} ({path.stat().st_size / 1e9:.2f} GB) ...")
|
|
api.upload_file(path_or_fileobj=str(path), path_in_repo=path.name,
|
|
repo_id=repo, repo_type="model")
|
|
print(f"done: https://huggingface.co/{repo}")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0],
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog=__doc__)
|
|
parser.add_argument("--size", choices=sorted(SIZES), default="1.5b")
|
|
parser.add_argument("--source", help="Local weights directory, or a HF repo id. "
|
|
"Defaults to the upstream repo for --size.")
|
|
parser.add_argument("--out", default="out", help="Directory for the built GGUFs.")
|
|
parser.add_argument("--precision", action="append", choices=["bf16", "q8_0", "q4_k"],
|
|
help="Repeatable. Defaults to all three.")
|
|
parser.add_argument("--gguf", help="Path to audiocpp_gguf; searched under build*/ if omitted.")
|
|
parser.add_argument("--cache", help="Hugging Face download cache directory.")
|
|
parser.add_argument("--verify-with", metavar="AUDIOCPP_CLI",
|
|
help="Score each built package with tools/asr_wer.py using this CLI.")
|
|
parser.add_argument("--verify-backend", default="cuda", choices=["cpu", "cuda", "metal", "vulkan"],
|
|
help="Backend for --verify-with. Quantized packages score differently per "
|
|
"backend, so this is recorded in the label.")
|
|
parser.add_argument("--assets", help="Clip directory for --verify-with; defaults to the "
|
|
"LibriSpeech clips in assets/asr_validation/librispeech.")
|
|
parser.add_argument("--upload", metavar="REPO", nargs="?", const="",
|
|
help="Publish the built GGUFs. Bare --upload uses the repo named in "
|
|
"the model spec for this size.")
|
|
args = parser.parse_args()
|
|
|
|
size = SIZES[args.size]
|
|
precisions = args.precision or ["bf16", "q8_0", "q4_k"]
|
|
converter = find_converter(args.gguf)
|
|
out = pathlib.Path(args.out)
|
|
out.mkdir(parents=True, exist_ok=True)
|
|
|
|
source = fetch_source(args.source or size["source"], args.cache)
|
|
staging = out / "sidecars"
|
|
staged = stage_sidecars(source, staging)
|
|
print(f"source: {source}\nconverter: {converter}\nsidecars: {len(staged)} files")
|
|
|
|
built = []
|
|
for precision in precisions:
|
|
output = out / f"vibevoice-asr-streaming-{size['slug']}-{precision}.gguf"
|
|
print(f"\n{precision} -> {output.name}")
|
|
convert(converter, source, staging, output, precision)
|
|
print(f" {output.stat().st_size / 1e9:.2f} GB")
|
|
if args.verify_with:
|
|
verify(args.verify_with, output, args.assets, args.verify_backend)
|
|
built.append(output)
|
|
|
|
shutil.rmtree(staging, ignore_errors=True)
|
|
|
|
if args.upload is None:
|
|
print("\nBuilt:")
|
|
for path in built:
|
|
print(f" {path}")
|
|
print("\nNothing was uploaded. Re-run with --upload to publish.")
|
|
return 0
|
|
|
|
upload(args.upload or size["repo"], built)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|