Skip to content

feat: Add Audio8-ASR-0.1B community model port (audio8_asr) - #337

Open
gqf2008 wants to merge 3 commits into
0xShug0:mainfrom
gqf2008:feat/audio8-asr
Open

feat: Add Audio8-ASR-0.1B community model port (audio8_asr)#337
gqf2008 wants to merge 3 commits into
0xShug0:mainfrom
gqf2008:feat/audio8-asr

Conversation

@gqf2008

@gqf2008 gqf2008 commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #336.

Native audio.cpp port of Audio8/Audio8-ASR-0.1B as community family audio8_asr: a compact multilingual offline ASR (en / zh / yue / ja / ko / fr / de; 324M params total, ~103M LM). Architecture: Whisper 128-mel frontend → Qwen3-ASR audio encoder → 4× pre-norm residual MLP tower → adaptive avg-pool (merge factor 4) → LayerNorm + Linear(1024→512) → 8-layer Qwen2-style decoder (hidden 512, tied embeddings, vocab 151936, RoPE 1e6).

Design points:

  • Encoder + frontend are 100% reused from qwen3_asr via a small renaming TensorSource (audio_encoder.*model.audio_tower.* / model.multi_modal_projector.{linear_1,linear_2}), so the C++ encoder implementation is exercised unchanged; audiocpp_add_model(... DEPENDS qwen3_asr) links it.
  • Decoder reuses the shared QwenCausalDecoder with use_qk_norm=false, per-projection Q/K/V attention biases, tied LM head (lm_head = embed_tokens).
  • Projector (tower + adaptive pool + projection) is the only new graph code; the adaptive average pool replicates torch.nn.functional.adaptive_avg_pool1d windowing exactly.
  • Parity detail: the reference processor emits bfloat16 mel; the audio8 frontend rounds mel to bf16 (RNE) before encoding so fp32 pipelines see the reference values.
  • License: checkpoint is CC-BY-NC-4.0. Following the mms_forced_aligner precedent, package_defaults.download is unsupported (local conversion only, no release GGUF redistribution); the safetensors package points at the upstream HF repo and loads directly in the runtime.

Verification

Build (macOS, Apple M4):

cmake -S . -B build/debug -DCMAKE_BUILD_TYPE=Debug -DENGINE_ENABLE_OPENMP=OFF -DGGML_OPENMP=OFF \
    -DAUDIOCPP_MODEL_SET=custom -DAUDIOCPP_MODELS=qwen3_asr,audio8_asr \
    -DENGINE_BUILD_TESTS=ON -DENGINE_BUILD_MODEL_TESTS=ON
cmake --build build/debug --target audiocpp_cli --target audiocpp_gguf -j 8
cmake --build build/debug --target test_audio8_asr_units --target test_audio8_asr_golden_transcription -j 8

Convert + run:

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

audiocpp_cli --task asr --family audio8_asr \
    --model models/Audio8-ASR-0.1B-GGUF/audio8-asr-0.1b-q8_0.gguf --audio assets/resources/a.wav

Parity evidence (greedy, fp32 HF trust_remote_code reference vs audio.cpp Q8_0 GGUF, exact text match on both backends):

Audio Reference (fp32) audio.cpp (Q8_0, Metal) audio.cpp (Q8_0, 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 identical
61 s clip, fixed 30 s windows per-window reference transcripts identical, space-joined

The F16 GGUF also matches exactly, and the safetensors package loads directly (no conversion) with the same output.

Tests:

  • test_audio8_asr_units — token-count formula (matches reference-observed 1407→176 / 595→74 / 3000→375, clamp-to-1 edge, zero-frame rejection) and bf16 RNE rounding. ctest -R audio8 passes.
  • test_audio8_asr_golden_transcription — end-to-end golden transcript via the loader registry; exits 125 (skip) when weights are absent.
  • python3 tools/check_loader_catalog_sync.py --self-test and the repo check both pass; audiocpp_cli --list-loaders --json emits audio8_asr with asr: [offline].

Timing: 14.07 s audio transcribed in ~2.7 s wall including model load and session setup (~175% CPU, Metal backend); 61 s in ~6.5 s.

Measured performance (Release build, Apple M4, Metal, Q8_0 GGUF):

Audio Session wall 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

Peak RSS ~1.0 GB per clip and ~1.3 GB for a three-window transcription
(Metal buffers + per-window-shape graph pools on top of 345 MB of weights;
CPU backend measures the same ~1.0 GB). Note for maintainers: the server
memory guard estimates this package at weights x 1.5 + 128 MB ~ 645 MB, which
underestimates the observed Metal peak by ~1.6-2x — possibly relevant to the
recent memory-guard hardening work. The encoder dominates (~64% of session
time); its Metal shaders are build-type-insensitive, so Debug and Release
measure within 3%.

Reference/validation tooling committed: tools/community_models/audio8_asr_reference.py (fp32 reference + golden dumps) and audio8_asr_stages.py (mel / encoder / projector staged tensors).

Model Used

Role Model
Implementation Claude Code + GLM5.3-Flash 辅助开发 (assisted development)

Known limitations

  • Offline only; no streaming, no word timestamps, no hotword boosting (decode-time logit bias is future work).
  • Audio longer than 30 s is transcribed in fixed 30 s windows with space-joined text (no VAD segmentation).
  • CC-BY-NC-4.0: local conversion only; GGUF must not be redistributed, so no release GGUF package is advertised by default.

Native port of Audio8-ASR-0.1B as community family audio8_asr: a compact
multilingual ASR (en/zh/yue/ja/ko/fr/de) whose Qwen3-ASR audio encoder is
loaded through the existing qwen3_asr implementation via a renaming tensor
source, followed by a new MLP-tower + adaptive-pool projector graph into an
8-layer Qwen2-style decoder (shared QwenCausalDecoder with use_qk_norm=false
and attention biases). The reference mel bfloat16 rounding is replicated for
parity.

The checkpoint is CC-BY-NC-4.0, so the package catalog ships no release GGUF
(users convert locally from the HF snapshot, following the
mms_forced_aligner precedent); the safetensors package loads directly.

Parity: greedy transcripts match the HF trust_remote_code reference exactly
on assets/resources/a.wav and sample_16k.wav with the Q8_0 GGUF on Metal and
CPU backends; a 61 s clip matches per-window reference transcripts.
Covered by test_audio8_asr_units (token-count formula, bf16 rounding) and
test_audio8_asr_golden_transcription (end-to-end, skipped without weights).

Closes 0xShug0#336
Review round 1 (independent reviewer + PR feedback):

- Remove the stray friend declaration for the anonymous-namespace loader:
  MSVC resolves the name to the incomplete namespace-scope class declared
  by the friend and fails the shared_ptr conversion (Windows CI), while
  clang picked the anonymous-namespace definition.
- Size transcription windows from config.max_audio_samples in the input
  sample domain (rate-correct at any input sample rate; sub-16 kHz inputs
  no longer overflow the encoder position table) and fold tails shorter
  than 0.5 s into the previous window.
- Release the resident weights file blob after the session's weight stores
  upload (mirrors qwen3_asr; roughly halves session RSS).
- Split weight storage options: audio_encoder_weight_type is limited to
  native/f32/f16 like the qwen3_asr encoder path; audio8_asr.weight_type
  governs decoder/adapter and accepts the full set. Options are parsed
  with the shared runtime helpers that fail loudly on malformed values,
  and the arena knobs are now declared in the model spec.
- Require tokenizer.json (the legacy vocab/merges branch could never
  load), validate model_type inside parse_config, drop the duplicated
  create_task_session guards, unify the two language lists (no 'Auto' —
  the port passes no language hint), and order the tensor-source reverse
  rename map longest-prefix-first so diagnostics list exact names.
- Converter rejects sharded safetensors checkpoints instead of silently
  converting the last shard, and skips sidecar copies onto themselves.
- Drop the WebUI catalog entry: the GGUF packages are
  download.kind=unsupported (CC-BY-NC-4.0, local conversion only), so the
  entry only offered a dead install button (mms_forced_aligner precedent
  has no catalog entry).
- Golden test now asserts the raw transcript verbatim in addition to the
  normalized comparison; model test targets gate on
  'audio8_asr IN_LIST AUDIOCPP_LINKED_MODELS' per the a76ec04 convention.

Reviewed-by: independent fresh-context reviewer (APPROVE-WITH-NITS);
parity re-verified after the changes: exact transcripts on Metal and CPU
for a.wav and sample_16k.wav (q8_0 GGUF), 61 s clips at 16 kHz and 8 kHz.

Closes 0xShug0#336
@gqf2008

gqf2008 commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

Independent review (fresh-context reviewer, APPROVE-WITH-NITS) + fork CI round 1 done. Fix commit f7bc36b addresses the findings:

  • Windows CI failure: stray friend class Audio8ASRLoader; made MSVC resolve the name to an incomplete namespace-scope class (clang picked the anonymous-namespace definition) — declaration removed.
  • Window sizing now derives from max_audio_samples in the input sample domain (rate-correct at 8/16/24/44.1/48 kHz; sub-16 kHz no longer overflows the encoder position table) with a 0.5 s minimum-tail fold.
  • Resident weights file blob is released after weight-store upload (mirrors qwen3_asr; ~halves session RSS).
  • Weight storage split: audio_encoder_weight_type limited to native/f32/f16 (qwen3 encoder precedent); malformed option values now fail loudly via the shared runtime parsers; arena knobs declared in the spec.
  • Converter rejects sharded safetensors checkpoints (previously silently converted the last shard); same-file sidecar copies skipped.
  • tokenizer.json now required (dead legacy branch removed), model_type validated in parse_config, duplicated task/mode guards collapsed, language lists unified, reverse rename map ordered longest-prefix-first.
  • WebUI catalog entry removed: GGUF packages are download.kind=unsupported (CC-BY-NC-4.0, local conversion only), so the entry only offered a dead install button (matches the mms_forced_aligner precedent).
  • Golden test asserts the raw transcript verbatim; test targets gate on audio8_asr IN_LIST AUDIOCPP_LINKED_MODELS (a76ec04 convention).

Re-verified after the changes: exact-transcript parity on Metal + CPU (a.wav, sample_16k.wav, q8_0 GGUF), 61 s clips at 16 kHz and 8 kHz, unit + golden tests green, check_loader_catalog_sync.py ok. Round-2 fork CI (ci/audio8-asr @ f7bc36b) running.

@0xShug0 0xShug0 added the new model Request for new model support label Aug 29, 2026
@0xShug0

0xShug0 commented Aug 29, 2026

Copy link
Copy Markdown
Owner

@gqf2008 @jasonchen31 Thank you both for contributing new models! I’ll mostly be focusing on fixing bugs on my TODO list this weekend, so I may not get to testing the PRs right away.

One thing you could look at in the meantime is whether there are any shared and useful logic that can be promoted into the framework to reduce duplication across the PRs. I’m also happy to handle that part if it helps make the implementations cleaner and easier to maintain.

@gqf2008

gqf2008 commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the direction — I compared #337 (audio8_asr) against #333 (audio8_tts) and the qwen3_asr baseline. Concrete overlap, ranked by dedup value:

  1. Greedy decoder graph scaffolding (~600-900 lines per model, biggest win). Both PRs (and qwen3_asr/thinker.cpp) already build layers through the shared QwenCausalDecoder module, but each re-rolls the same wrapper: prefill graph with ggml_cpy/ggml_dup_tensor K/V copy-out + output pinning, static-cache decode graph, write_qwen_cached_step_mask, TransformerKVState handoff via import_state, argmax+EOS greedy loop, and the build/trim_backend_pools retry. A framework-level GreedyQwenDecoderRuntime parameterized by (weight prefix, QwenCausalDecoderConfig tweaks like use_qk_norm, tied lm_head, weights-binder callback) would collapse my thinker.cpp (905 lines) and feat: Supporting Audio8_TTS models #333's dual slow/fast decoders substantially. My thinker was itself adapted from the qwen3_asr baseline, so a promotion could later be backported to that family too.

  2. Qwen BPE tokenizer bootstrapping (~30 lines per model). Each port repeats: LlamaBpeTokenizerSpec{pre_type=Qwen2, tokenizer_config/vocab/merges/tokenizer_json from the bundle} plus a require_added_token_id(bundle, "<|...|>") scan over tokenizer.json added_tokens (mine, feat: Supporting Audio8_TTS models #333's, and qwen3_asr's copies). Two small helpers (load_qwen_bpe_tokenizer(bundle) / require_added_token_id(bundle, content)) would remove all three copies.

  3. Prefix-renaming TensorSource (~80 lines, one copy so far). feat: Add Audio8-ASR-0.1B community model port (audio8_asr) #337 needs it to reuse the qwen3_asr encoder implementation for the Audio8 checkpoint (audio_encoder.*model.audio_tower.* / multi_modal_projector.linear_*). I implemented it as RenamingTensorSource : engine::assets::TensorSource — generic enough to promote next to the existing make_prefixed_tensor_source. Only promote if a second consumer shows up, otherwise it can stay family-local.

Not worth promoting from my side: the MLP-tower + adaptive-pool projector (audio8-specific), the offline 30 s windowed-session loop (audio8-specific semantics; granite5asr's chunker already covers the general case), and the loader boilerplate (explicit classes seem to be house style).

Happy to do 1+2 as a follow-up refactoring PR (sequenced after the model PRs merge so the promotion has both consumers in-tree), or leave it to you as you suggested — whichever keeps your review load lower. If you'd rather take it, I'll keep #337 as-is and rebase onto the promoted helpers once they land.

…8_asr

Apple M4 / Metal / Q8_0 GGUF: ~18x realtime in-session (RTF ~0.055),
encoder-dominated; peak RSS ~1.0 GB per clip and ~1.3 GB for a
three-window transcription. Data gathered per the contributing guide's
request for timing/RTF/RSS notes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new model Request for new model support

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Add Audio8-ASR-0.1B community model port (audio8_asr)

2 participants