Skip to content
Merged
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
42 changes: 42 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1314,6 +1314,20 @@ audiocpp_add_model(granite5asr
granite_speech5_ctc
)

audiocpp_add_model(audio8_asr
SOURCES
src/community_models/audio8_asr/assets.cpp
src/community_models/audio8_asr/projector.cpp
src/community_models/audio8_asr/thinker.cpp
src/community_models/audio8_asr/session.cpp
INCLUDES
engine/community_models/audio8_asr/session.h
LOADERS
engine::community_models::audio8_asr::make_audio8_asr_loader
DEPENDS
qwen3_asr
)

audiocpp_add_model(vevo2
SOURCES
src/models/vevo2/ar.cpp
Expand Down Expand Up @@ -2325,6 +2339,34 @@ if (ENGINE_BUILD_TESTS)
target_link_libraries(test_granite5asr_golden_transcription PRIVATE OpenMP::OpenMP_CXX)
endif()

if (audio8_asr IN_LIST AUDIOCPP_LINKED_MODELS)
add_executable(test_audio8_asr_units
tests/audio8_asr/test_audio8_asr_units.cpp
)
target_link_libraries(test_audio8_asr_units PRIVATE engine_runtime ggml)
target_include_directories(test_audio8_asr_units PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests/unittests)
if (ENGINE_ENABLE_OPENMP)
target_link_libraries(test_audio8_asr_units PRIVATE OpenMP::OpenMP_CXX)
endif()

add_test(
NAME test_audio8_asr_units
COMMAND test_audio8_asr_units
)

add_executable(test_audio8_asr_golden_transcription
tests/audio8_asr/test_audio8_asr_golden_transcription.cpp
)
target_compile_definitions(test_audio8_asr_golden_transcription PRIVATE
ENGINE_REPO_ROOT="${CMAKE_CURRENT_SOURCE_DIR}"
)
target_link_libraries(test_audio8_asr_golden_transcription PRIVATE engine_runtime ggml)
target_include_directories(test_audio8_asr_golden_transcription PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
if (ENGINE_ENABLE_OPENMP)
target_link_libraries(test_audio8_asr_golden_transcription PRIVATE OpenMP::OpenMP_CXX)
endif()
endif()

add_executable(torch_bin_parity
tests/vibevoice/torch_bin_parity.cpp
)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ Community model ports live under `community_models` to make the ownership bounda

| Family | Task | Lang | Runtime | Contributor | What They Added |
|---|---|---|---|---|---|
| **audio8_asr** | ASR | en, zh, yue, ja, ko, fr, de | GGUF Q8, Safetensors | [@0xShug0](https://github.com/0xShug0) | [Audio8-ASR-0.1B](docs/community_models/audio8_asr.md) compact multilingual autoregressive ASR reusing the Qwen3-ASR encoder with an MLP-tower adapter and an 8-layer Qwen2-style decoder (CC-BY-NC, local conversion only) |
| **f5_tts** | TTS, Clone | en, ar (Habibi) | GGUF | [@tareko](https://github.com/tareko) | [F5-TTS](docs/community_models/f5_tts.md) flow-matching DiT synthesis and voice cloning, with Habibi Arabic aliases `habibi`/`habibi_tts` |
| **glm_tts** | TTS, Clone | zh, en | GGUF | Mirek [@mirek190](https://github.com/mirek190) | [GLM-TTS](docs/community_models/glm_tts.md) zero-shot synthesis and voice cloning support |
| **granite5asr** | ASR | en | GGUF Q8 | [@ampersandru](https://github.com/ampersandru) | [IBM Granite Speech 5.0 470M TurboCTC](docs/community_models/granite5asr.md) ultra-fast Conformer-CTC ASR with Shaw relative positional embeddings and ByteLevel BPE |
Expand Down
104 changes: 104 additions & 0 deletions docs/community_models/audio8_asr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Audio8-ASR-0.1B in audio.cpp

[Audio8-ASR-0.1B](https://huggingface.co/Audio8/Audio8-ASR-0.1B) is a compact
multilingual autoregressive ASR model (en / zh / yue / ja / ko / fr / de): a
Qwen3-ASR audio encoder adapted by an MLP tower into an 8-layer Qwen2-style
decoder with only ~103M language-model parameters (324M end-to-end). The
checkpoint ships under **CC-BY-NC-4.0**, so audio.cpp loads it from locally
converted weights only; the converted GGUF must not be redistributed.

## Architecture

- **Audio frontend**: 16 kHz Whisper log-mel, 128 bins, hop 160, n_fft 400
(shared with the `qwen3_asr` family). The reference processor emits
bfloat16 mel values; the audio8_asr frontend rounds to bfloat16 before
encoding to match.
- **Audio encoder**: Qwen3-ASR audio tower (d_model 896, 18 layers, 14 heads,
FFN 3584, 128 mel bins, output dim 1024) — bit-for-bit the same
architecture as `Qwen/Qwen3-ASR-0.6B`, loaded through the shared
`qwen3_asr` encoder implementation.
- **Adapter**: 4 pre-norm residual MLP blocks (1024 → 4096, erf GELU), a
final LayerNorm, then an adaptive average pool (merge factor 4 against the
mel-frame count) followed by LayerNorm + Linear(1024 → 512).
- **Decoder**: Qwen2-style causal LM, 8 layers, hidden 512, 8 heads,
head_dim 64, SwiGLU FFN 1408, tied embeddings over a 151,936 Qwen BPE
vocabulary, RoPE theta 1e6, RMSNorm eps 1e-6.
- **Prompt**: `<|user|><|begin_of_audio|><|audio|>×N<|end_of_audio|>Please
transcribe this audio.<|assistant|>` where
`N = max(floor(floor((floor(samples / hop) + 1) / 2) / merge_factor), 1)`
(all divisions integer).

## Usage

```bash
# Convert locally (requires the audiocpp_gguf tool and a self-downloaded
# checkpoint from the Audio8/Audio8-ASR-0.1B HF repository):
python tools/community_models/convert_audio8_asr.py \
--checkpoint models/Audio8-ASR-0.1B-hf \
--converter build/debug/bin/audiocpp_gguf \
--type q8_0 \
--output models/Audio8-ASR-0.1B-GGUF/audio8-asr-0.1b-q8_0.gguf

# Transcribe
audiocpp_cli --task asr --family audio8_asr \
--model models/Audio8-ASR-0.1B-GGUF/audio8-asr-0.1b-q8_0.gguf \
--audio sample.wav

# The safetensors package loads directly, no conversion required:
audiocpp_cli --task asr --family audio8_asr \
--model models/Audio8-ASR-0.1B-hf --audio sample.wav
```

## Parity

`tools/community_models/audio8_asr_reference.py` runs the Hugging Face
`trust_remote_code` reference (torch CPU, fp32) and
`tools/community_models/audio8_asr_stages.py` captures staged tensors (mel,
encoder output, projected audio embeddings) for comparison.

Greedy transcription on the repo test clips matched the fp32 reference
exactly with the Q8_0 GGUF on both the Metal and CPU backends:

| Audio | Reference (fp32) | audio.cpp (Q8_0 GGUF, Metal + CPU) |
|---|---|---|
| `assets/resources/a.wav` (5.95 s) | "This little work was finished in the year eighteen o three, and intended for immediate publication." | identical |
| `assets/resources/sample_16k.wav` (14.07 s) | "Some call me nature. Others call me Mother Nature. I've been here for over four point five billion years, twenty-two thousand five hundred times longer than you." | identical |

A 61-second clip transcribed through rate-correct 30-second windows matched
the per-window reference transcripts (also verified by an independent review
pass). `test_audio8_asr_golden_transcription` asserts the first row
end-to-end — run it manually once weights exist (it is not part of ctest,
matching the granite5asr golden-test convention); `test_audio8_asr_units`
covers the token-count formula and bfloat16 rounding and runs under ctest.

## Measured performance

Release build (`-DCMAKE_BUILD_TYPE=Release`), Apple M4, Metal backend,
Q8_0 GGUF (345 MB weights):

| Audio | Session wall (`session.wall_ms`) | Effective RTF | CLI wall (incl. ~2.2 s process + load) |
|---|---|---|---|
| 5.95 s | ~0.75 s | ~8x realtime | 2.2 s |
| 14.07 s | 757 ms | 18.6x realtime | 3.8 s |
| 61 s (3 windows) | 3.46 s | 17.6x realtime | 5.8 s |

The audio encoder dominates (~64% of session time; its Metal shaders are
insensitive to build type, so Debug and Release measure within 3%).
Peak RSS is ~1.0 GB for a single clip and ~1.3 GB for a three-window
transcription (Metal buffers + per-window-shape graph pools on top of the
345 MB of weights; CPU backend measures the same ~1.0 GB).

## Known limitations

- **Offline only**: no streaming mode, no word timestamps, no language-id
output.
- **30-second windows**: audio longer than 30 s is transcribed in fixed
windows sized at the input sample rate (0.5 s minimum tail folds into the
previous window) and space-joined, without VAD segmentation. Unlike the
single-pass reference, each window is peak-normalized independently, so
relative loudness across a window boundary can shift.
- **CC-BY-NC-4.0**: non-commercial use only; convert locally, do not
redistribute the converted GGUF. There is no release GGUF package and no
WebUI catalog entry (the package manager cannot download
unsupported-license packages).
- Hotword logit boosting from the reference implementation is not ported.
1 change: 1 addition & 0 deletions docs/community_models/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Practical expectations:

| Family | Task | Supported language(s) | Contributor | What They Added |
|---|---|---|---|---|
| **audio8_asr** | ASR | en, zh, yue, ja, ko, fr, de | [@0xShug0](https://github.com/0xShug0) | [Audio8-ASR-0.1B](audio8_asr.md) compact multilingual autoregressive ASR reusing the Qwen3-ASR encoder with an MLP-tower adapter and an 8-layer Qwen2-style decoder (CC-BY-NC, local conversion only) |
| **echo_tts** | TTS, voice cloning | en | Tym [@5uck1ess](https://github.com/5uck1ess), [@dignome](https://github.com/dignome) | [Echo-TTS](echo_tts.md) 44.1 kHz zero-shot voice cloning: 2.8B diffusion transformer in 80-D PCA space, decoded by the Fish S1-DAC autoencoder. Byte-level text, no phonemiser, no reference transcript |
| **f5_tts** | TTS, voice cloning | en, ar (Habibi) | Community | [F5-TTS](f5_tts.md) flow-matching DiT — M0 scaffolding, aliases `habibi`/`habibi_tts` |
| **glm_tts** | TTS, voice cloning | zh, en | Mirek [@mirek190](https://github.com/mirek190) | [GLM-TTS](glm_tts.md) zero-shot synthesis and voice cloning support |
Expand Down
31 changes: 31 additions & 0 deletions include/engine/community_models/audio8_asr/assets.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#pragma once

#include "engine/framework/assets/resource_bundle.h"
#include "engine/framework/tokenizers/llama_bpe.h"
#include "engine/models/qwen3_asr/assets.h"

#include <filesystem>
#include <memory>

#include "engine/community_models/audio8_asr/types.h"

namespace engine::community_models::audio8_asr {

struct Audio8ASRAssets {
// Bundle and config for this family (arkasr layout).
assets::ResourceBundle resources;
Audio8ASRConfig config;

// Qwen2 BPE tokenizer shared with the Qwen3-ASR family tooling.
std::shared_ptr<engine::tokenizers::LlamaBpeTokenizer> tokenizer;

// A Qwen3-ASR view over the same bundle so the audio encoder and Whisper
// frontend implementations can be reused unchanged. The weights source in
// this view renames Audio8 tensors to the prefixes those implementations
// expect (`audio_encoder.*` -> `model.audio_tower.*` etc.).
std::shared_ptr<const qwen3_asr::Qwen3ASRAssets> encoder_assets;
};

std::shared_ptr<const Audio8ASRAssets> load_audio8_asr_assets(const std::filesystem::path & model_path);

} // namespace engine::community_models::audio8_asr
41 changes: 41 additions & 0 deletions include/engine/community_models/audio8_asr/projector.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#pragma once

#include "engine/framework/assets/tensor_source.h"
#include "engine/framework/core/execution_context.h"
#include "engine/community_models/audio8_asr/types.h"

#include <cstddef>
#include <memory>

namespace engine::community_models::audio8_asr {

// Runs the Audio8 adapter tail over Qwen3-ASR encoder output: the residual
// MLP tower, an adaptive average pool down to the prompt's audio token count,
// and the LayerNorm + linear projector into the decoder hidden size.
class Audio8ProjectorRuntime {
public:
Audio8ProjectorRuntime(
std::shared_ptr<const assets::TensorSource> weights_source,
const Audio8TowerConfig & config,
core::ExecutionContext & execution,
size_t graph_arena_bytes,
size_t weight_context_bytes,
assets::TensorStorageType weight_storage_type);
~Audio8ProjectorRuntime();

Audio8ProjectorRuntime(const Audio8ProjectorRuntime &) = delete;
Audio8ProjectorRuntime & operator=(const Audio8ProjectorRuntime &) = delete;

// input: [encoder_tokens, tower.input_size] float values (token-major);
// returns [audio_tokens, tower.output_size] float values (token-major).
Audio8ASRAudioEmbeddings project(
const std::vector<float> & encoder_output,
int64_t encoder_tokens,
int64_t audio_tokens);

private:
struct Impl;
std::unique_ptr<Impl> impl_;
};

} // namespace engine::community_models::audio8_asr
66 changes: 66 additions & 0 deletions include/engine/community_models/audio8_asr/session.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#pragma once

#include "engine/framework/runtime/model.h"
#include "engine/framework/runtime/session_base.h"
#include "engine/community_models/audio8_asr/assets.h"
#include "engine/community_models/audio8_asr/projector.h"
#include "engine/community_models/audio8_asr/thinker.h"
#include "engine/models/qwen3_asr/audio_encoder.h"
#include "engine/models/qwen3_asr/frontend_whisper.h"

#include <memory>
#include <string>

namespace engine::community_models::audio8_asr {

class Audio8ASRSession final
: public runtime::RuntimeSessionBase
, public runtime::IOfflineVoiceTaskSession {
public:
Audio8ASRSession(
runtime::TaskSpec task,
runtime::SessionOptions options,
std::shared_ptr<const Audio8ASRAssets> assets);
~Audio8ASRSession() override;

std::string family() const override;
runtime::VoiceTaskKind task_kind() const override;
runtime::RunMode run_mode() const override;
void prepare(const runtime::SessionPreparationRequest & request) override;
runtime::TaskResult run(const runtime::TaskRequest & request) override;

private:
std::string transcribe_clip(const runtime::AudioBuffer & audio);
runtime::Transcript transcribe_audio(const runtime::AudioBuffer & audio);

runtime::TaskSpec task_;
std::shared_ptr<const Audio8ASRAssets> assets_;
qwen3_asr::Qwen3ASRWhisperFrontend frontend_;
qwen3_asr::Qwen3ASRAudioEncoderRuntime audio_encoder_;
Audio8ProjectorRuntime projector_;
Audio8ThinkerRuntime thinker_;
};

class Audio8ASRLoadedModel final : public runtime::ILoadedVoiceModel {
public:
Audio8ASRLoadedModel(
runtime::ModelMetadata metadata,
runtime::CapabilitySet capabilities,
std::shared_ptr<const Audio8ASRAssets> assets);

const runtime::ModelMetadata & metadata() const noexcept override;
const runtime::CapabilitySet & capabilities() const noexcept override;
std::unique_ptr<runtime::IVoiceTaskSession> create_task_session(
const runtime::TaskSpec & task,
const runtime::SessionOptions & options) const override;

private:
runtime::ModelMetadata metadata_;
runtime::CapabilitySet capabilities_;
std::shared_ptr<const Audio8ASRAssets> assets_;
};

std::unique_ptr<Audio8ASRLoadedModel> load_audio8_asr_model(const runtime::ModelLoadRequest & request);
std::shared_ptr<runtime::IVoiceModelLoader> make_audio8_asr_loader();

} // namespace engine::community_models::audio8_asr
40 changes: 40 additions & 0 deletions include/engine/community_models/audio8_asr/thinker.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#pragma once

#include "engine/framework/assets/tensor_source.h"
#include "engine/framework/core/execution_context.h"
#include "engine/community_models/audio8_asr/types.h"

#include <cstddef>
#include <memory>

namespace engine::community_models::audio8_asr {

// Greedy causal decoder for the Audio8 8-layer Qwen2-style LM. Audio
// embeddings are injected into the token embedding sequence at the prompt's
// audio placeholder positions before prefill.
class Audio8ThinkerRuntime {
public:
Audio8ThinkerRuntime(
std::shared_ptr<const assets::TensorSource> weights_source,
const Audio8ASRDecoderConfig & config,
core::ExecutionContext & execution,
size_t prefill_graph_arena_bytes,
size_t decode_graph_arena_bytes,
size_t weight_context_bytes,
assets::TensorStorageType weight_storage_type);
~Audio8ThinkerRuntime();

Audio8ThinkerRuntime(const Audio8ThinkerRuntime &) = delete;
Audio8ThinkerRuntime & operator=(const Audio8ThinkerRuntime &) = delete;

Audio8ASRGeneratedTokens generate(
const Audio8ASRPrompt & prompt,
const Audio8ASRAudioEmbeddings & audio_embeddings,
const Audio8ASRGenerationOptions & options);

private:
struct Impl;
std::unique_ptr<Impl> impl_;
};

} // namespace engine::community_models::audio8_asr
Loading
Loading