Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,18 @@ audiocpp_add_model(fun_asr_nano
engine::models::fun_asr_nano::make_fun_asr_nano_loader
)

audiocpp_add_model(sense_asr
SOURCES
src/community_models/sense_asr/assets.cpp
src/community_models/sense_asr/frontend.cpp
src/community_models/sense_asr/encoder.cpp
src/community_models/sense_asr/session.cpp
INCLUDES
engine/community_models/sense_asr/session.h
LOADERS
engine::community_models::sense_asr::make_sense_asr_loader
)

audiocpp_add_model(heartmula
SOURCES
src/models/heartmula/assets.cpp
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ Community model ports live under `community_models` to make the ownership bounda
| **moss_tts_local** | TTS, Clone, Ctrl | auto, optional language hint | GGUF | [@justinjohn0306](https://github.com/justinjohn0306) | MOSS-TTS-Local Transformer v1.5 support |
| **outetts** | TTS, Clone | en, ar, zh, nl, fr, de, it, ja, ko, lt, ru, es, pt, be, bn, ka, hu, lv, fa, pl, sw, ta, uk | GGUF | Mirek [@mirek190](https://github.com/mirek190) | Llama-OuteTTS-1.0-1B TTS and voice cloning support |
| **parakeet_tdt** | ASR | auto, bg, cs, da, de, el, en, es, et, fi, fr, hr, hu, it, lt, lv, mt, nl, pl, pt, ro, ru, sk, sl, sv, uk | GGUF F32/16/Q8, Stream | [@dleiferives](https://github.com/dleiferives) | [Parakeet-TDT 0.6B v3](docs/community_models/parakeet_tdt.md) offline, long-form, and buffered-streaming ASR support |
| **sense_asr** | ASR | auto, zh, en, yue, ja, ko, pt, ru, es, it, fr, de, nl, pl, tr, ar, hi, vi, th, id, ms, fa, nospeech | GGUF Q8, Stream | Community | [SenseVoice-Small](docs/community_models/sense_asr.md) offline/streaming SAN-M + CTC transcription with event/emotion/language tags and ITN |
| **vietneu_tts** | TTS, Clone | vi, en | GGUF | Phuoc [@phuocnguyen90](https://github.com/phuocnguyen90) | [VieNeu-TTS-v3-Turbo](docs/community_models/vietneu_tts.md) TTS and voice cloning support |

## Docker
Expand Down
23 changes: 23 additions & 0 deletions docs/build/linux.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,29 @@ Leave `CMAKE_CUDA_ARCHITECTURES` unset to build for the GPUs present at build ti
(`native`). Note that CMake caches the CUDA compiler: switching toolkits in an
existing build directory requires deleting `CMakeCache.txt` and `CMakeFiles/`.

### Building an old GPU with the helper script

The `scripts/build_linux.sh --backend cuda` script leaves `CMAKE_CUDA_ARCHITECTURES` unset, so it
builds for the GPU attached to the *build* machine (`native`). If you build on one machine and run
on another — or target an old GPU such as a Pascal compute-capability 6.1 device — pass
`--cuda-arch`, which forwards `-DCMAKE_CUDA_ARCHITECTURES` (and un-defines the sticky cache value so
a previous configure does not win):

```bash
scripts/build_linux.sh --backend cuda --cuda-arch 61 --target audiocpp_cli --target audiocpp_server
```

Pinning an architecture below 8.9 also turns SageAttention2 off at build time
(`external/ggml/src/ggml-cuda/CMakeLists.txt` gates it to `>= 89`), so no SM89+ kernels are packaged
and a 6.1 GPU will not hit an illegal-instruction core dump:
`scripts/build_linux.sh --backend cuda --cuda-arch 61` is equivalent to the working direct CMake
configure `cmake -S . -B build -DENGINE_ENABLE_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=61`.

You do **not** need `-DENGINE_ENABLE_NATIVE_CPU=OFF` for this: `GGML_NATIVE` only influences the CUDA
architecture as a *fallback* (`if (NOT DEFINED CMAKE_CUDA_ARCHITECTURES)` in the ggml CUDA CMake
file), so once `--cuda-arch`/`CMAKE_CUDA_ARCHITECTURES` is set, GPU architecture is fully determined
by it.

On WSL2, install the toolkit only — `cuda-toolkit-<version>` from the `wsl-ubuntu`
repo. The `cuda` and `cuda-drivers` metapackages pull a Linux display driver that
breaks the GPU passthrough provided by the Windows host driver.
Expand Down
1 change: 1 addition & 0 deletions docs/community_models/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ Practical expectations:
| **moss_tts_local** | TTS, voice cloning | auto, optional language hint | [@justinjohn0306](https://github.com/justinjohn0306) | [MOSS-TTS-Local Transformer v1.5](../models/moss_tts.md) support in the core model tree |
| **outetts** | TTS, voice cloning | en, ar, zh, nl, fr, de, it, ja, ko, lt, ru, es, pt, be, bn, ka, hu, lv, fa, pl, sw, ta, uk | Mirek [@mirek190](https://github.com/mirek190) | [Llama-OuteTTS-1.0-1B](outetts.md) TTS and voice cloning support |
| **parakeet_tdt** | ASR | auto, bg, cs, da, de, el, en, es, et, fi, fr, hr, hu, it, lt, lv, mt, nl, pl, pt, ro, ru, sk, sl, sv, uk | [@dleiferives](https://github.com/dleiferives) | [Parakeet-TDT 0.6B v3](parakeet_tdt.md) offline, long-form, and buffered-streaming ASR support |
| **sense_asr** | ASR | auto, zh, en, yue, ja, ko, pt, ru, es, it, fr, de, nl, pl, tr, ar, hi, vi, th, id, ms, fa, nospeech | Community | [SenseVoice-Small](sense_asr.md) offline/streaming SAN-M + CTC transcription with event/emotion/language tags and ITN |
| **vietneu_tts** | TTS, voice cloning | vi, en | Phuoc [@phuocnguyen90](https://github.com/phuocnguyen90) | [VieNeu-TTS-v3-Turbo](vietneu_tts.md) TTS and voice cloning support |
233 changes: 233 additions & 0 deletions docs/community_models/sense_asr-pr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
# 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/community_models/sense_asr.md` (+ rows in `docs/community_models/models.md`, `README.md`) |
| WebUI | `webui/webui.py` — `MODEL_PROFILES["sense_asr"]` with `supports_streaming: true`, `MODEL_HINTS_EN["sense_asr"]`; catalog entry in `webui/configs/models_catalog.json` |

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.

## WebUI integration fix (this PR)

The model was missing from `MODEL_PROFILES` in `webui/webui.py`, causing the
streaming toggle to not appear in the ASR tab. Added:

- `MODEL_PROFILES["sense_asr"]` with `"supports_streaming": True` and input hint
- `MODEL_HINTS_EN["sense_asr"]` with English hint

Documentation updated in `docs/community_models/sense_asr.md` with 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"]` ✅ |
| Required files sync | `required_files.json` | `sensevoice_small_q8` → `SenseVoice-Small-GGUF/sensevoice-small-q8-audiocpp-v1.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.
Loading
Loading