Skip to content

Commit 66358c5

Browse files
authored
feat: B1 packaging — one-command download, standalone convert, CI-friendly CMake (#3000)
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.
1 parent 2775666 commit 66358c5

4 files changed

Lines changed: 178 additions & 0 deletions

File tree

runtime/llama.cpp/CMakeLists.txt

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Standalone, CI-friendly build for the FunASR llama.cpp runtime.
2+
#
3+
# Fetches a pinned llama.cpp (which provides ggml + llama) and builds the runtime
4+
# binaries against it — no manual copying into a llama.cpp checkout, no hardcoded
5+
# paths. Linux / macOS / Windows, x64 / arm64. Static libs -> self-contained binaries.
6+
#
7+
# cmake -B build -DCMAKE_BUILD_TYPE=Release
8+
# cmake --build build -j # binaries land in build/bin/
9+
#
10+
# Build against a local checkout instead of fetching:
11+
# -DFETCHCONTENT_SOURCE_DIR_LLAMA=/path/to/llama.cpp
12+
cmake_minimum_required(VERSION 3.16)
13+
project(funasr-llamacpp CXX C)
14+
15+
set(CMAKE_CXX_STANDARD 17)
16+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
17+
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) # static deps -> self-contained binaries
18+
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
19+
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
20+
set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
21+
endif()
22+
23+
include(FetchContent)
24+
set(LLAMA_BUILD_TESTS OFF CACHE BOOL "" FORCE)
25+
set(LLAMA_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
26+
set(LLAMA_BUILD_TOOLS OFF CACHE BOOL "" FORCE)
27+
set(LLAMA_BUILD_SERVER OFF CACHE BOOL "" FORCE)
28+
set(LLAMA_CURL OFF CACHE BOOL "" FORCE)
29+
FetchContent_Declare(llama
30+
GIT_REPOSITORY https://github.com/ggml-org/llama.cpp.git
31+
GIT_TAG 8086439a4cea94c71a5dfb8fe4ad1546aebd640f)
32+
FetchContent_MakeAvailable(llama)
33+
34+
find_package(Threads REQUIRED)
35+
set(FUNASR_COMMON ${CMAKE_CURRENT_SOURCE_DIR}/funasr-common)
36+
37+
function(funasr_add name src)
38+
add_executable(${name} ${src})
39+
target_include_directories(${name} PRIVATE ${FUNASR_COMMON})
40+
target_link_libraries(${name} PRIVATE ${ARGN} Threads::Threads)
41+
install(TARGETS ${name} RUNTIME DESTINATION bin)
42+
endfunction()
43+
44+
funasr_add(llama-funasr-cli fun-asr-nano/funasr-cli/funasr-cli.cpp llama ggml)
45+
funasr_add(llama-funasr-encoder fun-asr-nano/funasr-encoder/funasr-encoder.cpp ggml)
46+
funasr_add(llama-funasr-embd fun-asr-nano/funasr-embd/funasr-embd.cpp llama ggml)
47+
funasr_add(llama-funasr-sensevoice sensevoice/funasr-sensevoice/funasr-sensevoice.cpp ggml)
48+
funasr_add(llama-funasr-paraformer paraformer/funasr-paraformer/funasr-paraformer.cpp ggml)
49+
funasr_add(llama-funasr-vad funasr-vad/funasr-vad.cpp ggml)

runtime/llama.cpp/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@ and the audio embeddings are injected into it via `llama_decode`'s embedding inp
2828
(the llava/mtmd mechanism). See each model's README for the architecture diagram,
2929
build/convert/run quickstart, validation numbers, and gotchas.
3030

31+
## Download pre-built GGUF (fastest — no Python ML env)
32+
```bash
33+
./download-funasr-model.sh sensevoice # or: paraformer | nano | fsmn-vad
34+
```
35+
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`.
36+
37+
## Build (standalone, CI-friendly)
38+
```bash
39+
cmake -B build -DCMAKE_BUILD_TYPE=Release # fetches pinned llama.cpp; static, self-contained
40+
cmake --build build -j # -> build/bin/llama-funasr-* (all tools)
41+
```
3142
## Build (shared)
3243
```bash
3344
git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/usr/bin/env python3
2+
"""One-step FunASR -> GGUF converter for the llama.cpp runtime.
3+
4+
Downloads a model checkpoint from Hugging Face (or ModelScope) and exports it to a
5+
GGUF the C++ runtime can load — no manual `export_*.py` invocation, mirroring
6+
whisper.cpp's `convert` flow.
7+
8+
python convert-funasr-to-gguf.py sensevoice # -> sensevoice-small.gguf
9+
python convert-funasr-to-gguf.py paraformer --wtype f16 # -> paraformer-f16.gguf
10+
python convert-funasr-to-gguf.py fsmn-vad # -> fsmn-vad.gguf
11+
python convert-funasr-to-gguf.py nano-encoder --wtype f16 # -> funasr-encoder-f16.gguf
12+
python convert-funasr-to-gguf.py sensevoice --src modelscope
13+
14+
Fun-ASR-Nano also needs the Qwen3-0.6B LLM GGUF (a standard llama.cpp conversion of the
15+
HF checkpoint) — see `--help` notes; this tool covers the audio encoder/adaptor half.
16+
"""
17+
import argparse, glob, os, subprocess, sys
18+
19+
# model key -> (hf repo, modelscope id, export script, needs am.mvn, default out stem)
20+
MODELS = {
21+
"sensevoice": ("FunAudioLLM/SenseVoiceSmall",
22+
"iic/SenseVoiceSmall",
23+
"export_sensevoice_gguf.py", True, "sensevoice-small"),
24+
"paraformer": ("funasr/paraformer-zh",
25+
"iic/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-pytorch",
26+
"export_paraformer_gguf.py", True, "paraformer"),
27+
"fsmn-vad": ("funasr/fsmn-vad",
28+
"iic/speech_fsmn_vad_zh-cn-16k-common-pytorch",
29+
"export_vad_gguf.py", True, "fsmn-vad"),
30+
"nano-encoder": ("FunAudioLLM/Fun-ASR-Nano-2512",
31+
"iic/Fun-ASR-Nano",
32+
"export_encoder_gguf.py", False, "funasr-encoder"),
33+
}
34+
35+
def find_script(name):
36+
"""Locate an export_*.py relative to this file (works in every repo layout)."""
37+
here = os.path.dirname(os.path.abspath(__file__))
38+
hits = glob.glob(os.path.join(here, "**", name), recursive=True)
39+
if not hits:
40+
sys.exit(f"error: cannot find {name} next to {here}")
41+
return hits[0]
42+
43+
def download(key, src):
44+
hf_repo, ms_id, _, _, _ = MODELS[key]
45+
if src == "modelscope":
46+
from modelscope import snapshot_download
47+
return snapshot_download(ms_id)
48+
from huggingface_hub import snapshot_download
49+
return snapshot_download(hf_repo)
50+
51+
def pick(d, *names):
52+
for n in names:
53+
hits = glob.glob(os.path.join(d, "**", n), recursive=True)
54+
if hits:
55+
return hits[0]
56+
return None
57+
58+
def main():
59+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
60+
ap.add_argument("model", choices=list(MODELS), help="which model to convert")
61+
ap.add_argument("--src", choices=["hf", "modelscope"], default="hf", help="checkpoint source")
62+
ap.add_argument("--wtype", choices=["f32", "f16"], default="f32",
63+
help="matmul weight dtype in the GGUF (norm/bias stay f32)")
64+
ap.add_argument("--outdir", default=".", help="output directory")
65+
ap.add_argument("--out", default=None, help="output filename (default: <stem>[-f16].gguf)")
66+
a = ap.parse_args()
67+
68+
hf_repo, ms_id, script_name, needs_mvn, stem = MODELS[a.model]
69+
print(f"[1/3] downloading {a.model} from {a.src} "
70+
f"({ms_id if a.src=='modelscope' else hf_repo}) ...", flush=True)
71+
d = download(a.model, a.src)
72+
pt = pick(d, "model.pt", "model.pb", "*.pt")
73+
mvn = pick(d, "am.mvn") if needs_mvn else None
74+
if not pt: sys.exit(f"error: no model.pt under {d}")
75+
if needs_mvn and not mvn: sys.exit(f"error: no am.mvn under {d}")
76+
77+
out = a.out or f"{stem}{'-f16' if a.wtype=='f16' else ''}.gguf"
78+
out = os.path.join(a.outdir, out)
79+
os.makedirs(a.outdir, exist_ok=True)
80+
81+
cmd = [sys.executable, find_script(script_name), "--model_pt", pt, "--out", out]
82+
if needs_mvn: cmd += ["--mvn", mvn]
83+
# export_vad_gguf.py has no --wtype flag (tiny model, always f32)
84+
if script_name != "export_vad_gguf.py": cmd += ["--wtype", a.wtype]
85+
print(f"[2/3] exporting -> {out}", flush=True)
86+
subprocess.run(cmd, check=True)
87+
sz = os.path.getsize(out) / 1e6
88+
print(f"[3/3] done: {out} ({sz:.1f} MB)")
89+
if a.model == "nano-encoder":
90+
print("note: Fun-ASR-Nano also needs the Qwen3-0.6B LLM GGUF — convert it with "
91+
"llama.cpp's convert_hf_to_gguf.py on the HF checkpoint, then optionally quantize "
92+
"(Q8_0 recommended). Run with: llama-funasr-cli --enc <this> -m <qwen3.gguf> -a a.wav")
93+
94+
if __name__ == "__main__":
95+
main()
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env bash
2+
# Download pre-converted FunASR GGUF models from Hugging Face — one command, no Python ML env.
3+
# ./download-funasr-model.sh sensevoice [outdir]
4+
# ./download-funasr-model.sh paraformer | nano | fsmn-vad
5+
# ASR models also pull fsmn-vad.gguf (needed for built-in --vad long-audio segmentation).
6+
set -euo pipefail
7+
usage(){ echo "usage: $0 {sensevoice|paraformer|nano|fsmn-vad} [outdir]"; exit 1; }
8+
[ $# -ge 1 ] || usage
9+
MODEL="$1"; OUT="${2:-funasr-gguf}"
10+
case "$MODEL" in
11+
sensevoice) REPO="FunAudioLLM/SenseVoiceSmall-GGUF" ;;
12+
paraformer) REPO="FunAudioLLM/Paraformer-GGUF" ;;
13+
nano) REPO="FunAudioLLM/Fun-ASR-Nano-GGUF" ;;
14+
fsmn-vad|vad) REPO="FunAudioLLM/fsmn-vad-GGUF" ;;
15+
*) usage ;;
16+
esac
17+
command -v hf >/dev/null 2>&1 || { echo "install first: pip install -U huggingface_hub"; exit 1; }
18+
mkdir -p "$OUT"
19+
echo "downloading $REPO ..."; hf download "$REPO" --include "*.gguf" --local-dir "$OUT"
20+
if [ "$MODEL" != "fsmn-vad" ] && [ "$MODEL" != "vad" ]; then
21+
echo "downloading FSMN-VAD (for --vad) ..."; hf download FunAudioLLM/fsmn-vad-GGUF --include "*.gguf" --local-dir "$OUT"
22+
fi
23+
echo "done -> $OUT"; ls -1 "$OUT"/*.gguf

0 commit comments

Comments
 (0)