From 66358c577e96d0968b01335d0f4ed17578a3519b Mon Sep 17 00:00:00 2001 From: zhifu gao Date: Sat, 20 Jun 2026 14:14:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20B1=20packaging=20=E2=80=94=20one-comman?= =?UTF-8?q?d=20download,=20standalone=20convert,=20CI-friendly=20CMake=20(?= =?UTF-8?q?#3000)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the runtime a downloadable product (whisper.cpp-style), no manual export needed: - download-funasr-model.sh: one command to pull pre-converted GGUF from Hugging Face (FunAudioLLM/*-GGUF), incl. fsmn-vad for built-in --vad. No Python ML env required. - convert-funasr-to-gguf.py: one-step HF/ModelScope checkpoint -> GGUF (wraps the validated export_*.py; discovered relative to itself so it works in any layout). - CMakeLists.txt: standalone, CI-friendly top-level build. FetchContent pins llama.cpp (provides ggml + llama), static libs -> self-contained binaries in build/bin/, no hardcoded paths; builds on Linux/macOS/Windows x64/arm64. `cmake -B build && cmake --build build -j`. - README: download-first + standalone-build quickstart; links to the HF GGUF repos. Verified on Linux: all targets build via FetchContent (static, no shared-lib deps); hf download -> run reproduces canonical output. Additive; runtime/llama.cpp/ only. --- runtime/llama.cpp/CMakeLists.txt | 49 +++++++++++ runtime/llama.cpp/README.md | 11 +++ runtime/llama.cpp/convert-funasr-to-gguf.py | 95 +++++++++++++++++++++ runtime/llama.cpp/download-funasr-model.sh | 23 +++++ 4 files changed, 178 insertions(+) create mode 100644 runtime/llama.cpp/CMakeLists.txt create mode 100644 runtime/llama.cpp/convert-funasr-to-gguf.py create mode 100755 runtime/llama.cpp/download-funasr-model.sh diff --git a/runtime/llama.cpp/CMakeLists.txt b/runtime/llama.cpp/CMakeLists.txt new file mode 100644 index 000000000..ca19c1996 --- /dev/null +++ b/runtime/llama.cpp/CMakeLists.txt @@ -0,0 +1,49 @@ +# Standalone, CI-friendly build for the FunASR llama.cpp runtime. +# +# Fetches a pinned llama.cpp (which provides ggml + llama) and builds the runtime +# binaries against it — no manual copying into a llama.cpp checkout, no hardcoded +# paths. Linux / macOS / Windows, x64 / arm64. Static libs -> self-contained binaries. +# +# cmake -B build -DCMAKE_BUILD_TYPE=Release +# cmake --build build -j # binaries land in build/bin/ +# +# Build against a local checkout instead of fetching: +# -DFETCHCONTENT_SOURCE_DIR_LLAMA=/path/to/llama.cpp +cmake_minimum_required(VERSION 3.16) +project(funasr-llamacpp CXX C) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) # static deps -> self-contained binaries +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE) +endif() + +include(FetchContent) +set(LLAMA_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(LLAMA_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(LLAMA_BUILD_TOOLS OFF CACHE BOOL "" FORCE) +set(LLAMA_BUILD_SERVER OFF CACHE BOOL "" FORCE) +set(LLAMA_CURL OFF CACHE BOOL "" FORCE) +FetchContent_Declare(llama + GIT_REPOSITORY https://github.com/ggml-org/llama.cpp.git + GIT_TAG 8086439a4cea94c71a5dfb8fe4ad1546aebd640f) +FetchContent_MakeAvailable(llama) + +find_package(Threads REQUIRED) +set(FUNASR_COMMON ${CMAKE_CURRENT_SOURCE_DIR}/funasr-common) + +function(funasr_add name src) + add_executable(${name} ${src}) + target_include_directories(${name} PRIVATE ${FUNASR_COMMON}) + target_link_libraries(${name} PRIVATE ${ARGN} Threads::Threads) + install(TARGETS ${name} RUNTIME DESTINATION bin) +endfunction() + +funasr_add(llama-funasr-cli fun-asr-nano/funasr-cli/funasr-cli.cpp llama ggml) +funasr_add(llama-funasr-encoder fun-asr-nano/funasr-encoder/funasr-encoder.cpp ggml) +funasr_add(llama-funasr-embd fun-asr-nano/funasr-embd/funasr-embd.cpp llama ggml) +funasr_add(llama-funasr-sensevoice sensevoice/funasr-sensevoice/funasr-sensevoice.cpp ggml) +funasr_add(llama-funasr-paraformer paraformer/funasr-paraformer/funasr-paraformer.cpp ggml) +funasr_add(llama-funasr-vad funasr-vad/funasr-vad.cpp ggml) diff --git a/runtime/llama.cpp/README.md b/runtime/llama.cpp/README.md index 5d8a59f55..5b7d7b56a 100644 --- a/runtime/llama.cpp/README.md +++ b/runtime/llama.cpp/README.md @@ -28,6 +28,17 @@ and the audio embeddings are injected into it via `llama_decode`'s embedding inp (the llava/mtmd mechanism). See each model's README for the architecture diagram, build/convert/run quickstart, validation numbers, and gotchas. +## Download pre-built GGUF (fastest — no Python ML env) +```bash +./download-funasr-model.sh sensevoice # or: paraformer | nano | fsmn-vad +``` +Pre-converted GGUF on Hugging Face: [SenseVoiceSmall-GGUF](https://huggingface.co/FunAudioLLM/SenseVoiceSmall-GGUF) · [Paraformer-GGUF](https://huggingface.co/FunAudioLLM/Paraformer-GGUF) · [Fun-ASR-Nano-GGUF](https://huggingface.co/FunAudioLLM/Fun-ASR-Nano-GGUF) · [fsmn-vad-GGUF](https://huggingface.co/FunAudioLLM/fsmn-vad-GGUF). Or convert yourself with `convert-funasr-to-gguf.py`. + +## Build (standalone, CI-friendly) +```bash +cmake -B build -DCMAKE_BUILD_TYPE=Release # fetches pinned llama.cpp; static, self-contained +cmake --build build -j # -> build/bin/llama-funasr-* (all tools) +``` ## Build (shared) ```bash git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp diff --git a/runtime/llama.cpp/convert-funasr-to-gguf.py b/runtime/llama.cpp/convert-funasr-to-gguf.py new file mode 100644 index 000000000..77ca9013e --- /dev/null +++ b/runtime/llama.cpp/convert-funasr-to-gguf.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""One-step FunASR -> GGUF converter for the llama.cpp runtime. + +Downloads a model checkpoint from Hugging Face (or ModelScope) and exports it to a +GGUF the C++ runtime can load — no manual `export_*.py` invocation, mirroring +whisper.cpp's `convert` flow. + + python convert-funasr-to-gguf.py sensevoice # -> sensevoice-small.gguf + python convert-funasr-to-gguf.py paraformer --wtype f16 # -> paraformer-f16.gguf + python convert-funasr-to-gguf.py fsmn-vad # -> fsmn-vad.gguf + python convert-funasr-to-gguf.py nano-encoder --wtype f16 # -> funasr-encoder-f16.gguf + python convert-funasr-to-gguf.py sensevoice --src modelscope + +Fun-ASR-Nano also needs the Qwen3-0.6B LLM GGUF (a standard llama.cpp conversion of the +HF checkpoint) — see `--help` notes; this tool covers the audio encoder/adaptor half. +""" +import argparse, glob, os, subprocess, sys + +# model key -> (hf repo, modelscope id, export script, needs am.mvn, default out stem) +MODELS = { + "sensevoice": ("FunAudioLLM/SenseVoiceSmall", + "iic/SenseVoiceSmall", + "export_sensevoice_gguf.py", True, "sensevoice-small"), + "paraformer": ("funasr/paraformer-zh", + "iic/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-pytorch", + "export_paraformer_gguf.py", True, "paraformer"), + "fsmn-vad": ("funasr/fsmn-vad", + "iic/speech_fsmn_vad_zh-cn-16k-common-pytorch", + "export_vad_gguf.py", True, "fsmn-vad"), + "nano-encoder": ("FunAudioLLM/Fun-ASR-Nano-2512", + "iic/Fun-ASR-Nano", + "export_encoder_gguf.py", False, "funasr-encoder"), +} + +def find_script(name): + """Locate an export_*.py relative to this file (works in every repo layout).""" + here = os.path.dirname(os.path.abspath(__file__)) + hits = glob.glob(os.path.join(here, "**", name), recursive=True) + if not hits: + sys.exit(f"error: cannot find {name} next to {here}") + return hits[0] + +def download(key, src): + hf_repo, ms_id, _, _, _ = MODELS[key] + if src == "modelscope": + from modelscope import snapshot_download + return snapshot_download(ms_id) + from huggingface_hub import snapshot_download + return snapshot_download(hf_repo) + +def pick(d, *names): + for n in names: + hits = glob.glob(os.path.join(d, "**", n), recursive=True) + if hits: + return hits[0] + return None + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("model", choices=list(MODELS), help="which model to convert") + ap.add_argument("--src", choices=["hf", "modelscope"], default="hf", help="checkpoint source") + ap.add_argument("--wtype", choices=["f32", "f16"], default="f32", + help="matmul weight dtype in the GGUF (norm/bias stay f32)") + ap.add_argument("--outdir", default=".", help="output directory") + ap.add_argument("--out", default=None, help="output filename (default: [-f16].gguf)") + a = ap.parse_args() + + hf_repo, ms_id, script_name, needs_mvn, stem = MODELS[a.model] + print(f"[1/3] downloading {a.model} from {a.src} " + f"({ms_id if a.src=='modelscope' else hf_repo}) ...", flush=True) + d = download(a.model, a.src) + pt = pick(d, "model.pt", "model.pb", "*.pt") + mvn = pick(d, "am.mvn") if needs_mvn else None + if not pt: sys.exit(f"error: no model.pt under {d}") + if needs_mvn and not mvn: sys.exit(f"error: no am.mvn under {d}") + + out = a.out or f"{stem}{'-f16' if a.wtype=='f16' else ''}.gguf" + out = os.path.join(a.outdir, out) + os.makedirs(a.outdir, exist_ok=True) + + cmd = [sys.executable, find_script(script_name), "--model_pt", pt, "--out", out] + if needs_mvn: cmd += ["--mvn", mvn] + # export_vad_gguf.py has no --wtype flag (tiny model, always f32) + if script_name != "export_vad_gguf.py": cmd += ["--wtype", a.wtype] + print(f"[2/3] exporting -> {out}", flush=True) + subprocess.run(cmd, check=True) + sz = os.path.getsize(out) / 1e6 + print(f"[3/3] done: {out} ({sz:.1f} MB)") + if a.model == "nano-encoder": + print("note: Fun-ASR-Nano also needs the Qwen3-0.6B LLM GGUF — convert it with " + "llama.cpp's convert_hf_to_gguf.py on the HF checkpoint, then optionally quantize " + "(Q8_0 recommended). Run with: llama-funasr-cli --enc -m -a a.wav") + +if __name__ == "__main__": + main() diff --git a/runtime/llama.cpp/download-funasr-model.sh b/runtime/llama.cpp/download-funasr-model.sh new file mode 100755 index 000000000..87288bce3 --- /dev/null +++ b/runtime/llama.cpp/download-funasr-model.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Download pre-converted FunASR GGUF models from Hugging Face — one command, no Python ML env. +# ./download-funasr-model.sh sensevoice [outdir] +# ./download-funasr-model.sh paraformer | nano | fsmn-vad +# ASR models also pull fsmn-vad.gguf (needed for built-in --vad long-audio segmentation). +set -euo pipefail +usage(){ echo "usage: $0 {sensevoice|paraformer|nano|fsmn-vad} [outdir]"; exit 1; } +[ $# -ge 1 ] || usage +MODEL="$1"; OUT="${2:-funasr-gguf}" +case "$MODEL" in + sensevoice) REPO="FunAudioLLM/SenseVoiceSmall-GGUF" ;; + paraformer) REPO="FunAudioLLM/Paraformer-GGUF" ;; + nano) REPO="FunAudioLLM/Fun-ASR-Nano-GGUF" ;; + fsmn-vad|vad) REPO="FunAudioLLM/fsmn-vad-GGUF" ;; + *) usage ;; +esac +command -v hf >/dev/null 2>&1 || { echo "install first: pip install -U huggingface_hub"; exit 1; } +mkdir -p "$OUT" +echo "downloading $REPO ..."; hf download "$REPO" --include "*.gguf" --local-dir "$OUT" +if [ "$MODEL" != "fsmn-vad" ] && [ "$MODEL" != "vad" ]; then + echo "downloading FSMN-VAD (for --vad) ..."; hf download FunAudioLLM/fsmn-vad-GGUF --include "*.gguf" --local-dir "$OUT" +fi +echo "done -> $OUT"; ls -1 "$OUT"/*.gguf