voice_audio_multi/tools/models/prepare_vibevoice_asr_streaming_gguf.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

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())