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
228 lines
11 KiB
Markdown
228 lines
11 KiB
Markdown
# Add SenseVoice-Small (`sense_asr`) — community ASR port
|
||
|
||
Native audio.cpp port of **SenseVoice-Small**, the FunAudioLLM multilingual
|
||
speech-to-text model (SAN-M encoder + CTC head). Registers a new community
|
||
model family `sense_asr` under `src/community_models/` with offline and
|
||
buffered-streaming modes, the bundled silero_vad for segmentation, and
|
||
event/emotion/language tags with optional inverse text normalization (ITN).
|
||
|
||
Closes the porting track for
|
||
`/workspace/SenseVoice/runtime/llama.cpp/sensevoice-server/`.
|
||
|
||
---
|
||
|
||
## Summary of changes
|
||
|
||
| Area | Files |
|
||
|---|---|
|
||
| New model family | `src/community_models/sense_asr/` (`assets.cpp`, `frontend.cpp`, `encoder.cpp`, `session.cpp`, `loader.cpp`) |
|
||
| Headers | `include/engine/community_models/sense_asr/` (`assets.h`, `frontend.h`, `encoder.h`, `session.h`, `loader.h`, `types.h`) |
|
||
| Catalog | `model_specs/sense_asr.json` (family `sense_asr`, status `community`, package `sensevoice_small_q8`) |
|
||
| Build | `CMakeLists.txt` — `audiocpp_add_model(sense_asr ...)` with `engine::community_models::sense_asr::make_sense_asr_loader` |
|
||
| Docs | `docs/models/sense_asr.md` (+ rows in `README.md`) |
|
||
| WebUI | Native catalog entry in `webui/configs/models_catalog.json`; controls are driven by model-spec metadata |
|
||
|
||
The port adapts the engine (80-mel Kaldi-compatible filterbank + LFR, the
|
||
50-block SAN-M encoder, CTC collapse, SentencePiece detok) from the reference
|
||
`sensevoice-server.cpp`. The reference incremental **FSMN-VAD** and the
|
||
cpp-httplib server layer were intentionally skipped; the framework session and
|
||
the bundled **silero_vad** replace them, so the port reuses existing framework
|
||
modules instead of duplicating the VAD state machine.
|
||
|
||
Following the review that preceded this change, the following framework-facing
|
||
options were wired and validated:
|
||
|
||
- `enable_itn` now selects the query-token embedding (token `14`/`withitn` vs
|
||
`15`/`woitn`), matching the reference `textnorm_dict`.
|
||
- `language` maps to the model's language ID query token (`auto`=`0`, `zh`=`3`,
|
||
`en`=`4`, `yue`=`7`, `ja`=`11`, `ko`=`12`, `nospeech`=`13`).
|
||
- Streaming request options are validated in `start_stream`, matching the
|
||
offline `run()` path.
|
||
- Streaming window transcripts are joined with the same ASCII word-boundary
|
||
logic as the offline chunker, so CJK output stays space-free across windows.
|
||
|
||
---
|
||
|
||
## Exact build commands
|
||
|
||
Single-model custom build (this PR was built and tested with a CPU backend):
|
||
|
||
```bash
|
||
cmake -S . -B build/sense -DCMAKE_BUILD_TYPE=Release \
|
||
-DAUDIOCPP_MODEL_SET=custom -DAUDIOCPP_MODELS=sense_asr
|
||
cmake --build build/sense --target audiocpp_cli --parallel $(nproc)
|
||
cmake --build build/sense --target audiocpp_server --parallel $(nproc)
|
||
```
|
||
|
||
The full-model-set build (`scripts/build_linux.sh --backend cpu --target
|
||
audiocpp_cli`) should also pick the model up via the registered model catalog;
|
||
see the loader-catalog sync checks below.
|
||
|
||
## Model paths / package
|
||
|
||
- Model-manager package: **`sensevoice_small_q8`** (Q8 GGUF)
|
||
`tools/model_manager_v2.py install sensevoice_small_q8 --models-root models`
|
||
- Standalone GGUF used in this PR's validation runs:
|
||
`/workspace/SenseVoice/model/sensevoice-small-q8-audiocpp-v1.gguf`
|
||
(254 MB, exported by the reference runtime's `export_sensevoice_gguf.py`)
|
||
|
||
## Exact run commands
|
||
|
||
Offline:
|
||
|
||
```bash
|
||
audiocpp_cli --task asr --family sense_asr \
|
||
--model /workspace/SenseVoice/model/sensevoice-small-q8-audiocpp-v1.gguf \
|
||
--backend cpu --threads 8 \
|
||
--audio /workspace/SenseVoice/runtime/llama.cpp/tests/sample.wav \
|
||
--request-option audio_chunk_mode=none
|
||
```
|
||
|
||
Buffered streaming (raw 16 kHz S16 PCM on stdin, 3 s windows):
|
||
|
||
```bash
|
||
audiocpp_cli --task asr --family sense_asr \
|
||
--model /workspace/SenseVoice/model/sensevoice-small-q8-audiocpp-v1.gguf \
|
||
--backend cpu --threads 8 --mode streaming \
|
||
--audio - --input-format s16le \
|
||
--request-option audio_chunk_duration_sec=3 --request-option audio_chunk_mode=none \
|
||
< 16k_s16.pcm
|
||
```
|
||
|
||
Live server streaming:
|
||
|
||
```bash
|
||
audiocpp_server --config app/server/example.json --port 4096
|
||
curl -s -N -X POST 'http://127.0.0.1:4096/v1/audio/transcriptions/live?model=sense-asr' \
|
||
-H 'Transfer-Encoding: chunked' --data-binary @16k_s16.pcm
|
||
```
|
||
|
||
## Generated output artifacts / validation results
|
||
|
||
Test clip `sample.wav` (~6 s, 16 kHz mono Chinese speech).
|
||
|
||
| Run | Output |
|
||
|---|---|
|
||
| Reference `llama-funasr-sensevoice` (same GGUF) | `我想问我在滨海新区有房。` |
|
||
| audio.cpp offline (`audio_chunk_mode=none`) | `我想问我在滨海新区有房。` |
|
||
| audio.cpp offline, `enable_itn=false` | `我想问我在滨海新区有房` |
|
||
| audio.cpp offline, `language=zh` | `我想问我在滨海新区有房。` |
|
||
| audio.cpp streaming (3 s windows) | partials `我想问。` → `我在滨海新区有房。`, final `我想问。我在滨海新区有房。` |
|
||
| audio.cpp offline with VAD auto-chunking (default) | `我想问。我在滨海新区有房。` |
|
||
|
||
**Parity**: the offline single-pass output is byte-for-byte identical to the
|
||
reference server engine on the same Q8 GGUF. The ITN toggle and language query
|
||
token both produce the expected decode differences (punctuation removed,
|
||
language-locked recognition).
|
||
|
||
## Path / loader-catalog sync
|
||
|
||
```bash
|
||
python3 tools/check_loader_catalog_sync.py --self-test # OK
|
||
python3 tools/check_loader_catalog_sync.py # ok: runtime loaders, model_specs, model_manager_v2 in sync
|
||
./build/sense/bin/audiocpp_cli --list-loaders | grep sense_asr
|
||
# sense_asr: asr (offline|streaming)
|
||
```
|
||
|
||
## Backend tested
|
||
|
||
- **CPU** (this PR's validation environment). Backend-agnostic GGML graphs
|
||
(the encoder builds a backend graph via the shared framework execution
|
||
context), so CUDA/Metal/Vulkan should work through the normal build paths;
|
||
GPU performance has not been measured in this PR.
|
||
|
||
## Native WebUI integration
|
||
|
||
SenseVoice is exposed through `webui/configs/models_catalog.json`. The embedded
|
||
Svelte UI consumes the catalog and schema-v1 model metadata directly, including
|
||
its streaming mode and request controls. Verified server launch commands:
|
||
|
||
```bash
|
||
# WebUI-enabled server
|
||
audiocpp_server --ui --backend cpu \
|
||
--config <(echo '{"models":[{"id":"sense_asr","family":"sense_asr","path":"models/SenseVoice-Small-GGUF/sensevoice-small-q8-audiocpp-v1.gguf","task":"asr","mode":"streaming"}]}')
|
||
|
||
# Headless API server
|
||
audiocpp_server --backend cpu \
|
||
--config <(echo '{"models":[{"id":"sense_asr","family":"sense_asr","path":"models/SenseVoice-Small-GGUF/sensevoice-small-q8-audiocpp-v1.gguf","task":"asr","mode":"streaming"}],"ui":false}')
|
||
|
||
# Offline transcription
|
||
curl -X POST http://127.0.0.1:8080/v1/audio/transcriptions \
|
||
-F 'model=sense_asr' -F 'file=@assets/resources/3.wav'
|
||
|
||
# Streaming transcription (SSE)
|
||
curl -X POST http://127.0.0.1:8080/v1/audio/transcriptions/live \
|
||
-H 'Accept: text/event-stream' \
|
||
-H 'Transfer-Encoding: chunked' \
|
||
-F 'model=sense_asr' -F 'file=@assets/resources/3.wav'
|
||
```
|
||
|
||
## WebUI & server verification results
|
||
|
||
| Check | Command | Result |
|
||
|-------|---------|--------|
|
||
| C++ loader registered | `./build/sense/bin/audiocpp_cli --list-loaders` | `sense_asr: asr (offline\|streaming)` ✅ |
|
||
| Offline CLI transcription | `audiocpp_cli --task asr --family sense_asr --model ... --audio librispeech.wav` | `text_output=Concord returned to its place, amidst the tents.` ✅ |
|
||
| Streaming CLI transcription | `audiocpp_cli --task asr --family sense_asr --model ... --mode streaming --audio librispeech.wav` | `partial_text=...` then `text_output=Concord returned to its place amidst the tents.` ✅ |
|
||
| Server /v1/models endpoint | `curl http://127.0.0.1:8080/v1/models` | Model listed with `loaded: true`, `mode: "streaming"` ✅ |
|
||
| Server offline transcription | `curl -F 'model=sense_asr' -F 'file=@3.wav' /v1/audio/transcriptions` | Returns transcript with timing (RTF ~0.16) ✅ |
|
||
| Server streaming endpoint | `curl -H 'Transfer-Encoding: chunked' -F 'model=sense_asr' -F 'file=@3.wav' /v1/audio/transcriptions/live` | Returns `400: live transcription requires chunked body` (expected - client must stream) ✅ |
|
||
| WebUI catalog entry | `models_catalog.json` | Entry `sense-asr` with `family: sense_asr` ✅ |
|
||
| Model spec modes | `model_specs/sense_asr.json` | `"modes": ["offline", "streaming"]` ✅ |
|
||
| Package spec | `model_specs/sense_asr.json` | `sensevoice_small_q8` resolves to the standalone Q8 GGUF ✅ |
|
||
| Loader-catalog sync | `python3 tools/check_loader_catalog_sync.py` | OK ✅ |
|
||
|
||
All verifications run on `build/sense` (custom `AUDIOCPP_MODEL_SET=custom -DAUDIOCPP_MODELS=sense_asr` CPU build).
|
||
|
||
## Timing / RTF / RSS notes
|
||
|
||
Audio.cpp CLI, Q8 GGUF, CPU backend, 8 threads. All runs on a 12-thread Linux
|
||
x86-64 container.
|
||
|
||
| Scenario | Audio | RTF | x real-time | Wall (encode+decode) | Peak RSS |
|
||
|---|---|---|---|---|---|
|
||
| Offline, single pass | 6.0 s | 0.036 | **27.9x** | ~215 ms | 279 MiB |
|
||
| Offline, VAD auto-chunk (default) | 6.0 s | 0.033 | 30.2x | — | 279 MiB |
|
||
| Offline, long-form, VAD auto-chunk | 24.0 s | 0.035 | 28.8x | — | 281 MiB |
|
||
| Buffered streaming, 3 s windows ×2 | 6.0 s (fed) | — | — | ~116 ms per window | 278 MiB |
|
||
| 3-user request-sequence session | 6.0 s ×3 | 0.034–0.036 | — | — | 280 MiB |
|
||
|
||
Notes:
|
||
|
||
- RTF is measured by the CLI `--metrics` (`metrics.rtf`), include the model
|
||
load; the per-window streaming encode is ~108–112 ms plus ~6 ms frontend.
|
||
- **Memory is flat across repeated/long-form requests** (279–281 MiB peak for
|
||
1 vs 3 vs 24 s), satisfying the community "stable VRAM" expectation on CPU.
|
||
- Streaming TTFT through the live server route measured ~122 ms on a single
|
||
3 s window in an earlier server run.
|
||
|
||
## Known limitations
|
||
|
||
- `language` fully maps only the model's native tags (`zh`, `en`, `yue`, `ja`,
|
||
`ko`, `nospeech`); other advertised tags fall back to `auto`, consistent
|
||
with the reference `lid_dict`.
|
||
- Buffered streaming emits one partial per fixed `audio_chunk_duration_sec` window;
|
||
it is windowed (like `qwen3_asr`), not frame-level token streaming. A
|
||
6 s clip fed with 3 s windows yields 2 partials.
|
||
- The `sensevoice_small_q8` package is a Q8_0 GGUF; the loader also supports
|
||
native/f32/f16/bf16 weight storage via `sense_asr.weight_type`.
|
||
- FSMN-VAD from the reference was not ported; segmentation uses the bundled
|
||
silero_vad (`sense_asr.vad_model_path`).
|
||
|
||
## Review follow-ups actioned in this PR
|
||
|
||
1. `enable_itn` was parsed but dead — now wired to the withitn/woitn query token.
|
||
2. Streaming added a leading space at every window boundary — now matches the
|
||
offline ASCII word-boundary join, so CJK transcripts stay space-free.
|
||
3. `start_stream` did not validate request options — now does (parity with `run()`).
|
||
4. `language` was accepted but inert — now drives the language ID query token.
|
||
5. Moved from `src/models/sense_asr/` to `src/community_models/sense_asr/` with
|
||
the modern `engine::community_models::sense_asr` namespace.
|
||
|
||
## Suggested follow-ups
|
||
|
||
- Add `sense_asr` offline + streaming entries to
|
||
`tools/audiocpp_cli/audiocpp_cli_path_cases.json`.
|
||
- Measure CUDA/Metal/Vulkan RTF and VRAM once GPU hosts are available.
|
||
- Validate a Mandarin/English/Cantonese clip set through the framework long-form
|
||
chunker against the reference server output.
|