MOSS-Transcribe-Diarize

July 19, 2026 · View on GitHub

OpenMOSS's OpenMOSS-Team/MOSS-Transcribe-Diarize ported to transcribe.cpp. A 0.9B audio-LLM: a 24-layer Whisper-Medium audio encoder (d_model=1024, GELU, LayerNorm) feeds a 4x temporal merge (1024 -> 4096) and a VQAdaptor MLP bridge into a Qwen3-0.6B causal decoder (28 layers, hidden_size=1024, GQA 16/8 heads, rope_theta=1e6) via audio-token injection at <|audio_pad|> positions.

What it's for

Offline English and Chinese speech-to-text with optional speaker attribution. The model generates the canonical diarized format [start][Sxx]text[end] (e.g. [0.48][S01]Welcome[1.66]) via greedy decoding. The runtime parses those emergent text markers into clean full_text, segment rows, and—when diarize=ON—speaker IDs and speaker-turn rows. Built for long-form, multi-speaker audio. No translation; not a streaming model.

See OpenMOSS's model card for training data, intended use, and upstream evaluation. All of OpenMOSS's published metrics are Chinese multi-speaker diarization CER/cpCER; LibriSpeech test-clean is used here only as an English acceptance set.

Licensed Apache-2.0. Ported from upstream commit d7231bb, pinned 2026-07-12.

Memory and length

MOSS keeps the whole recording in memory while it transcribes, so RAM use grows with how long the audio is. Plan for roughly 85 MB of extra memory per minute of audio, on top of the model file itself — about 2.5 GB for a 30-minute clip and ~5 GB for an hour. It can handle recordings up to a couple of hours if you have the memory; anything longer is rejected with a clear error instead of being silently cut off. If you run low on memory, split long audio into shorter pieces.

Download

QuantizationDownloadSizeWER (LibriSpeech test-clean)
BF16MOSS-Transcribe-Diarize-BF16.gguf1.83 GB2.08%
F16MOSS-Transcribe-Diarize-F16.gguf1.83 GB2.07%
Q8_0MOSS-Transcribe-Diarize-Q8_0.gguf987 MB1.93%
Q6_KMOSS-Transcribe-Diarize-Q6_K.gguf768 MB1.96%
Q5_K_MMOSS-Transcribe-Diarize-Q5_K_M.gguf700 MB1.99%
Q4_K_MMOSS-Transcribe-Diarize-Q4_K_M.gguf617 MB2.59%

These WER values describe this dataset only, not a general quality ranking. A quant that scores slightly better here is not necessarily better in real-world use; dataset-specific decoding near-ties can make quantization noise help or hurt individual utterances.

WER measured on the full LibriSpeech test-clean split (2620 utterances) with the Whisper-style English normalizer and jiwer 3.x. MOSS emits the diarized format [start][Sxx]text[end]; the bracket spans are metadata and are de-diarized to a space (for both hypothesis and reference) before scoring, matching the author-repo reference runner. The same-manifest MOSS author-repo reference (bf16, greedy) lands at 2.07%, 95% bootstrap CI [1.82%, 2.40%]; the BF16 port lands at 2.08%, within +0.01pp of the reference and well inside the CI. Q4_K_M's higher 2.59% is not broad degradation but a handful of 4-bit tail failures (6 empty outputs, 5 English->Chinese language-drift utterances, 1 timestamp-token repetition loop); prefer Q5_K_M or higher if those matter. The runtime applies the same marker removal to full_text, so WER scoring and the public transcript agree. The pre-parsed inline marker string remains available verbatim via transcribe_raw_text().

Quick Start

cmake -B build
cmake --build build

build/bin/transcribe-cli \
  -m models/MOSS-Transcribe-Diarize/MOSS-Transcribe-Diarize-Q8_0.gguf \
  --diarize \
  samples/jfk.wav

If your audio is not already 16 kHz mono WAV, convert it first:

ffmpeg -i input.mp3 -ar 16000 -ac 1 output.wav

CLI flags:

  • -l en / -l zh (or omit for auto): English and Chinese are supported.
  • No --task / --target-language: the model is ASR-only, no translation.
  • Speaker attribution is off by default. Pass --diarize to populate speaker_id and speaker-turn rows; --no-diarize is the explicit off form.
  • Timestamp selection is independent: --timestamps segment or auto keeps parsed turn timing; --timestamps none returns attribution with zero times.
  • full_text is always clean marker-free text after a successful parse.

Performance

Cells are wall-clock latency (mean over 3 iterations after 1 warmup), with speedup over realtime in parentheses. Units: ms below 1 s, s above (2 decimal places).

Apple M4 Max

BackendSampleQ8_0Q4_K_M
Metaljfk (11.0s)388 ms (28.3×)369 ms (29.8×)
Metaldots (35.3s)1.27 s (27.8×)1.17 s (30.1×)
CPUjfk (11.0s)2.06 s (5.3×)2.37 s (4.6×)
CPUdots (35.3s)5.71 s (6.2×)5.84 s (6.0×)

macOS 26.5.1, transcribe.cpp e745720.

AMD Ryzen 7 PRO 4750U

BackendSampleQ8_0Q4_K_M
Vulkanjfk (11.0s)3.88 s (2.8×)3.68 s (3.0×)
Vulkandots (35.3s)11.38 s (3.1×)10.68 s (3.3×)
CPUjfk (11.0s)7.49 s (1.5×)7.06 s (1.6×)
CPUdots (35.3s)21.20 s (1.7×)19.22 s (1.8×)

Fedora Linux 43, transcribe.cpp e745720. Vulkan device: AMD Radeon Graphics (RADV RENOIR).

Benchmark reproduction:

uv run scripts/bench/run.py \
  --models moss-transcribe-diarize \
  --quants q8_0,q4_k_m \
  --samples jfk,dots \
  --backends metal,cpu,vulkan \
  --iters 3 --warmup 1 \
  --name moss-transcribe-diarize-publication

Numerical Validation

transcribe.cpp is validated tensor-by-tensor against the MOSS author repo (scripts/dump_reference_moss_author.py, trust_remote_code) on samples/jfk.wav with the strict CPU backend. The reference runs BF16 (torch, eager attention); the C++ path dequantizes BF16 weights to F32 and computes in F32, so C++ is the more precise side and the residual gap is a constant ~1-3% relative bf16-vs-f32 drift, not a bug. The transcript compare is dediarized (bracket metadata stripped to a space). Confirmed WER-neutral: on the first 100 test-clean utterances the C++ ref-dtype WER (1.40%) is bit-identical to the Oracle reference on the same subset (1.40%). Tolerances are pinned in tests/tolerances/moss.json with a _comment block naming the precision regime, the large-pre-normalization-activation maxes, and the encoder padding-trim contract. Last validated at commit 3f5e15c.

FieldValue
ReferenceMOSS author repo (OpenMOSS-Team/MOSS-Transcribe-Diarize)
Dump scriptscripts/dump_reference_moss_author.py
Manifesttests/golden/moss/moss-transcribe-diarize.manifest.json
Tolerancestests/tolerances/moss.json
Commanduv run scripts/validate.py all --family moss --variant moss-transcribe-diarize

Selected tensors (observed on CPU, strict backend; see the tolerance file for budgets and per-tensor notes):

TensorMax abs diffMean abs diffNotes
enc.mel.in9.872e-055.150e-06C++ MelFrontend vs reference feature extractor
enc.pos_add.out5.000e-022.200e-03Encoder input + positional embedding
enc.block.0.out1.200e-014.000e-03First Whisper encoder block
enc.block.23.out3.100e+038.000e-02Pre-final-LN residual; ref |max| ~3.6e3, bf16 rel error dominates max_abs (renormalized by enc.ln_post)
enc.ln_post.out1.300e+013.000e-03Encoder output LayerNorm
enc.merge.out1.500e+003.000e-034x temporal merge
enc.adaptor.out4.000e-011.200e-02VQAdaptor decoder handoff (rel_mean ~0.96%)
dec.audio_injected4.000e-017.000e-03Audio tokens scattered into the prompt
dec.block.0.out7.500e-019.500e-03First Qwen3 decoder block
dec.block.27.out3.200e+022.600e-01Pre-final-RMSNorm residual (bf16 accumulation over 28 layers)
dec.out_before_head8.500e+008.000e-02Pre-head hidden state
dec.logits_raw4.800e-017.200e-02Prefill logits; argmax preserved (transcript exact)
dec.logits_raw.gen85.000e-016.500e-02Greedy step 8 logits (KV-cache decode coverage)
dec.token_emb0.000e+000.000e+00Pure embedding lookup (pinned exact)

For the full porting writeup, see docs/porting/families/moss.md.

Reproduction

Convert

uv run --project scripts/envs/moss \
  scripts/convert-moss.py OpenMOSS-Team/MOSS-Transcribe-Diarize \
  --revision d7231bbae2587a4af278735eb765b318c4f64edd

Quantize

uv run scripts/quantize-all.py models/MOSS-Transcribe-Diarize/MOSS-Transcribe-Diarize-BF16.gguf

Validate

uv run scripts/validate.py all --family moss --variant moss-transcribe-diarize

Score WER

PRESET=BF16
uv run scripts/wer/run.py \
  --model models/MOSS-Transcribe-Diarize/MOSS-Transcribe-Diarize-${PRESET}.gguf \
  --manifest samples/wer/librispeech-test-clean.manifest.jsonl \
  --out reports/wer/MOSS-Transcribe-Diarize-${PRESET}.librispeech-test-clean.jsonl
uv run scripts/wer/score.py \
  reports/wer/MOSS-Transcribe-Diarize-${PRESET}.librispeech-test-clean.jsonl \
  --dediarize

Score WER against the MOSS author-repo reference

uv run --project scripts/envs/moss \
  scripts/wer/run_reference_moss_author.py \
    --model OpenMOSS-Team/MOSS-Transcribe-Diarize \
    --revision d7231bbae2587a4af278735eb765b318c4f64edd \
    --manifest samples/wer/librispeech-test-clean.manifest.jsonl \
    --out reports/wer/moss-transcribe-diarize-REF.librispeech-test-clean.jsonl
uv run scripts/wer/score.py \
  reports/wer/moss-transcribe-diarize-REF.librispeech-test-clean.jsonl \
  --dediarize