加载中…
加载中…
google/embeddinggemma-2English speech recognition from a frozen EmbeddingGemma 2: an 88M reader reads its audio as words of its own text-embedding space (LibriSpeech test-clean WER 1.74 greedy, 1.58 with the n-gram LM and reranker; Results).
EmbeddingGemma 2 ("eg2" in the model name) is used exactly as it is: its weights are not changed and it gives the same embeddings as before. A trained reader of 88M parameters turns the hidden states of its audio encoder and backbone into word-level CTC outputs, scored against EmbeddingGemma 2's own text embeddings of 407,736 words.
NOTICE).EmbeddingGemma 2 runs as its audio-language configuration (577M parameters in the paper): the audio encoder (two downsampling convolution layers and twelve conformer layers, USM architecture) turns Mel filterbank inputs into 25 soft tokens per second (d_A = 1536), the audio adapter projects them to the backbone's model dimension (d_M = 512), and the 24-layer bidirectional backbone processes them. The reader reads, for every soft token, the backbone input and its 24 layer outputs, the twelve conformer layers and the audio encoder output; it gives a blank logit and a unit vector that is compared by cosine with the word table, i.e. EmbeddingGemma 2 text embeddings (mean pooling, linear projection to d = 768, l2-normalisation) of every word.
Speech and words therefore meet in one embedding space. Each 40 ms soft token becomes a vector z(t) among the text embeddings of the vocabulary; greedy decoding emits the nearest word row when s · cos beats the blank logit, and the beam decoders keep the 16 nearest rows of every frame as candidates. The figure shows real cosines for one frame of "carriage" in a LibriSpeech dev-clean utterance, zoomed in twice: from the whole space to the target around z(t), then to the scores. The nearest row is "carriage" (cosine 0.46); the next ones, at 0.31-0.32, are words related in meaning (chariot, hearse) and spelling variants found in the vocabulary (carrage, carridge), all far below "carriage" and close to the blank head's score b(t)/s = 0.31.
| Part | Parameters | Trained here |
|---|---|---|
| EmbeddingGemma 2 audio encoder (2 downsampling conv + 12 conformer layers) | 304.8M | no (frozen) |
| EmbeddingGemma 2 audio adapter (1536 → 512) | 0.8M | no (frozen) |
| EmbeddingGemma 2 backbone (24 bidirectional layers, embedder included) | 271.0M | no (frozen) |
| EmbeddingGemma 2, audio-language configuration (577M in the paper) | 576.6M | |
| Reader (12 x 768 Transformer, word-CTC head) | 88.0M | yes |
| Total at inference (greedy, beam) | 664.6M | |
+ reranker heads (decoder="rerank" only) |
6.44M | yes |
+ no-LM reranker heads (decoder="rerank", profile="no_lm", instead of the ones above) |
6.44M | yes |
| Total with the second pass | 671.1M |
The word table (word_table.safetensors, 407,736 x 768, stored as row-wise INT8) is a cache of EmbeddingGemma 2
text embeddings, so it is not counted again. The vision encoder (167M) is not loaded.
pip install "torch>=2.11" torchvision "transformers>=5.20" safetensors numpy soundfile soxr
# torchvision: EmbeddingGemma 2's processor loads its image / video parts even for audio
pip install https://github.com/kpu/kenlm/archive/master.zip # only for the beam decoders with the n-gram LM
pip install "sentence-transformers>=6.1" # only for add_words / set_vocabulary
import soundfile as sf
from transformers import AutoModel
model = AutoModel.from_pretrained("lee1jun/LES-eg2-665M-asr-en", trust_remote_code=True).to("cuda").eval()
audio, sr = sf.read("speech.flac", dtype="float32")
model.transcribe(audio, sampling_rate=sr) # greedy: "the transcript"
model.transcribe(audio, sampling_rate=sr, decoder="beam") # + n-gram LM (downloads lee1jun/LES-eg2-asr-en-lm-4gram)
model.transcribe(audio, sampling_rate=sr, decoder="rerank") # + the beam's 32-best list re-ranked by the reranker heads
model.transcribe(audio, sampling_rate=sr, decoder="rerank", profile="no_lm") # second pass without the n-gram LM
model.transcribe([a1, a2, a3]) # batches, any lengths
r = model.transcribe(audio, sampling_rate=sr, return_timestamps=True)
# {"text": ..., "duration": ..., "words": [{"word", "start", "end", "confidence"}, ...]}
r = model.transcribe(audio, sampling_rate=sr, decoder="rerank", return_nbest=True, return_candidates=True)
# {"text": ..., "nbest": [{"text", "words", "score", "ctc", "lmf", "n", "reranker", "chosen"}, ...],
# "candidates": [{"time", "blank", "words": [(word, cosine, log p), ...]}, ...]}
EmbeddingGemma 2 (google/embeddinggemma-2, revision 914f7f89142e33e77833254d9c9b90c3cef7303b) is downloaded on
first use and used frozen, without its vision encoder; its weights are not part of this repository. Audio at any
sampling rate is resampled to 16 kHz.
| Option | Values |
|---|---|
decoder |
"greedy" (default), "beam" (word-CTC prefix beam search with n-gram shallow fusion), "rerank" (the beam's 32-best list re-ranked by the reranker heads) |
lm |
beam decoders: a KenLM binary / ARPA path or a Hub repo id; default lee1jun/LES-eg2-asr-en-lm-4gram |
profile |
beam decoders: "lm" (default) or "no_lm": no n-gram LM, so no download and no kenlm (alpha 0, beta 0; "rerank" uses its own reranker heads, see The reranker) |
alpha, beta, weights |
LM weight, word bonus and the N-best rescoring weights {"ctc", "lmf", "n", "reranker"}; defaults from decoding.json (alpha 0.15, beta 1.0, weights ctc 1, lmf 0.2, n 1, reranker 2, one setting for every domain) |
gate |
greedy: "config" (dev-calibrated blank gate: none in this build, so every frame is scanned; gate=31.0 skips about 20 % of LibriSpeech frames without changing LibriSpeech dev transcripts, but changes 0.5 % of AMI and 3 % of TED-LIUM dev transcripts), "exact" (provably lossless), None (scan every frame), a float |
bonus |
greedy: bonus added to words seen in training (seen_vocab.json) |
return_timestamps |
word start / end in seconds (spike-based, shifted back by the fitted delay 0.24 s; centres, not boundaries) and a confidence (cosine margin; below 0.02 is low) |
return_nbest |
beam decoders, inputs up to 40 s: adds "nbest", the scored 32-best list with the parts of each score (Your own re-ranking and search) |
return_candidates, top_k, candidates_blank_max, candidates_format |
every decoder: adds "candidates", each frame's top_k (16) nearest word rows with cosine and log-probability, for the frames whose blank probability is at most candidates_blank_max (0.999, the frames the beam search expands; None: every frame); candidates_format="arrays": numpy arrays over every frame (Your own re-ranking and search) |
return_embedding, embedding_dim |
adds "embedding", EmbeddingGemma 2's own audio embedding (below); embedding_dim 512 / 256 / 128 keeps the leading dimensions |
long_form_s, window_s, overlap_s |
audio over 40 s is decoded in 28 s windows overlapping by at least 4 s; long_form_s=None decodes one sequence |
batch_seconds |
padded audio per EmbeddingGemma 2 batch (default 300) |
model.second_pass(lm, decoder=...) returns the beam decoder as an object for repeated use (build it again after
changing the word table).
model.add_words(["embeddinggemma", "huggingface"]) # new vocabulary without retraining
model.set_vocabulary(["turn", "left", "right", "stop"]) # own word table: outputs only these words
model.reset_vocabulary() # back to the shipped table
The decoders can only output words of the table, so the table is also a vocabulary control. add_words extends it.
set_vocabulary(words) replaces it with exactly your list (commands, names, a domain glossary). Words already in the
table keep their rows; other words get rows from EmbeddingGemma 2 by the training recipe (needs
sentence-transformers). The reader is not retrained, and reset_vocabulary() restores the shipped table.
Speech outside the list comes out as the nearest listed words or, mostly, as nothing; there is no explicit
rejection. The beam decoders still apply the n-gram LM. model.save_pretrained(dir) saves the custom table (without
this card).
The model also returns EmbeddingGemma 2's own embedding (768-d, L2-normalised: the vector of
SentenceTransformer("google/embeddinggemma-2").encode(...)), alone or together with the transcript:
r = model.transcribe(audio, sampling_rate=sr, return_embedding=True) # {"text": ..., "embedding": tensor [768]}
e = model.embed_audio(audio, sampling_rate=sr) # embedding only (no reader, no word table)
q = model.embed_text("quarterly revenue guidance", prompt_name="query") # text in the same space
score = q @ e # cosine: text-to-audio search
model.embed_audio([a1, a2], dim=256) # Matryoshka sizes 512 / 256 / 128
return_embedding pools the EmbeddingGemma 2 pass the decoder already runs, so for audio up to 40 s the embedding
costs nothing extra (audio decoded in windows, and audio of 29-30 s, takes one more EmbeddingGemma 2 pass). Up to 30 s
the vectors equal sentence-transformers' (cosine ≥ 0.997, bf16 rounding). Longer audio is mean-pooled over 25 s
pieces so that the whole input counts, whereas sentence-transformers encodes only the first 30 s. embed_text takes
EmbeddingGemma 2's prompt names ("query", "document", ...; default none) or a literal prompt= and needs no
sentence-transformers.
| Model, decoding | Params | LS clean | LS other | TED-LIUM 3 | VoxPopuli | Earnings-22 | AMI | GigaSpeech | SPGISpeech | Mean |
|---|---|---|---|---|---|---|---|---|---|---|
| This model, greedy (default) | 665M (88M) | 1.74 | 4.51 | 4.15 | 7.02 | 13.31 | 11.18 | 9.60 | 3.58 | 6.89 |
This model, decoder="beam" (n-gram LM) |
665M (88M) | 1.61 | 4.12 | 3.99 | 6.77 | 13.07 | 10.77 | 9.47 | 3.56 | 6.67 |
This model, decoder="rerank" (n-gram LM + reranker) |
671M (94M) | 1.58 | 4.00 | 3.88 | 6.74 | 12.82 | 10.75 | 9.34 | 3.68 | 6.60 |
This model, decoder="beam", profile="no_lm" (no LM) |
665M (88M) | 1.74 | 4.51 | 4.13 | 6.98 | 13.24 | 11.14 | 9.58 | 3.57 | 6.86 |
This model, decoder="rerank", profile="no_lm" (no LM, own reranker) |
671M (94M) | 1.75 | 4.53 | 4.03 | 7.02 | 13.20 | 11.03 | 9.48 | 3.69 | 6.84 |
| nvidia/parakeet-tdt-0.6b-v2, greedy | 618M | 1.29 | 2.71 | 3.09 | 5.64 | 10.89 | 10.41 | 8.27 | 1.91 | 5.53 |
| nvidia/parakeet-ctc-1.1b, greedy | 1,063M | 1.51 | 3.25 | 3.19 | 6.27 | 12.90 | 14.67 | 8.86 | 3.67 | 6.79 |
| openai/whisper-large-v3, greedy | 1,543M | 1.50 | 3.12 | 3.56 | 8.66 | 10.89 | 14.69 | 8.46 | 2.68 | 6.70 |
"rerank"), EmbeddingGemma 2 being frozen."rerank" re-ranks the beam's 32-best list with the reranker heads and the weights in decoding.json."beam" and "rerank" give the research pipeline's transcript for 1,549 of 1,600 spot-checked decodes (200 utterances each of 4 test sets).profile="no_lm" (no LM download, no kenlm; "rerank" uses its own reranker heads) gives means of 6.86 ("beam") and 6.84 ("rerank"), against 6.67 and 6.60 with the LM: the n-gram LM brings most of the second pass's gain.Each 40 ms frame offers several word rows; the beam search takes the 16 nearest per frame (the figure shows the top 5 for four words of a LibriSpeech dev-other utterance). Greedy decoding takes the top row of every frame, the beam search with the n-gram LM and the reranker choose a path through the candidates, and the oracle is the best path the list contains.
The beam's 32-best list (31.5 distinct hypotheses on average) holds much better transcripts than the decoders pick. The table gives the WER if the best hypothesis among the first k were picked (oracle WER, same normaliser and scorer):
| Best of the first k | LS clean | LS other | TED-LIUM 3 | VoxPopuli | Earnings-22 | AMI | GigaSpeech | SPGISpeech | Mean |
|---|---|---|---|---|---|---|---|---|---|
k = 1 (decoder="beam") |
1.61 | 4.12 | 3.99 | 6.77 | 13.07 | 10.77 | 9.47 | 3.56 | 6.67 |
| k = 2 | 1.12 | 3.22 | 3.15 | 5.88 | 11.62 | 8.64 | 8.34 | 2.69 | 5.58 |
| k = 5 | 0.76 | 2.45 | 2.45 | 4.96 | 10.07 | 6.60 | 7.26 | 1.98 | 4.57 |
| k = 10 | 0.64 | 2.04 | 2.05 | 4.46 | 9.14 | 5.55 | 6.58 | 1.62 | 4.01 |
| k = 32 | 0.48 | 1.61 | 1.55 | 3.77 | 7.96 | 4.20 | 5.68 | 1.19 | 3.31 |
Choosing the best of the top 10 would lower the mean WER from 6.67 to 4.01. The included reranker recovers 0.07 of that gap (mean 6.60); see Your own re-ranking and search.
decoder="rerank" re-scores the beam's 32-best list: each hypothesis gets
score = 1 · ctc + 0.2 · lmf + 1 · n + 2 · reranker (the weights in decoding.json), and the largest wins. ctc is
the hypothesis' CTC log-likelihood over the frame candidates, lmf its n-gram log-probability (natural log, with the
end of sentence), n its word count and reranker the score of the reranker heads (reranker_heads.safetensors).
profile="no_lm"): reranker_heads_nolm.safetensors has the same architecture and
training, on 32-best lists decoded without any n-gram LM (alpha 0, beta 0), with the LM inputs (lmf and each
word's n-gram log-probability) set to zero; the consensus shares weight the hypotheses without the LM, and
score = ctc + 2 · reranker.The 32-best list holds much better transcripts than the decoders pick. Choosing the best hypothesis of the list would give a mean WER of 3.31 (best of the first 10: 4.01; AMI 10.75 → 4.20, Earnings-22 12.82 → 7.96; see the oracle table), against 6.60 with the included reranker, which recovers only 0.07 of the 2.66 points between the best beam (6.67) and the best of the first 10. Better re-ranking is where this model has the most room: a stronger language model, domain knowledge, or a better learned reranker can turn much of that gap into real gains, so please try your own. The decoders return their own scores, so you can recombine them, add your own, or run your own search:
import numpy as np
# 1. your own weights or features on the 32-best list (here an external LM score of each text, my_lm)
r = model.transcribe(audio, sampling_rate=sr, decoder="rerank", return_nbest=True)
best = max(r["nbest"], key=lambda h: h["ctc"] + 0.5 * h["lmf"] + h["n"] + 2 * h["reranker"] + my_lm(h["text"]))
# 2. hypotheses from anywhere (an LLM's correction, another recogniser), scored together with the 32-best list
s = model.score_hypotheses(audio, ["the corrected sentence"], sampling_rate=sr)
# the 32-best entries (the same scores as above) + yours: {..., "given": True, "in_nbest": False, "oov": []}
# 3. your own search over the frame candidates, without another audio pass
c = model.transcribe(audio, sampling_rate=sr, return_candidates=True, candidates_format="arrays")["candidates"]
c["logp"][c["ids"] == model.vocab.index("uh")] = -np.inf # e.g. forbid a word, or bias / re-weight "logp"
nbest = model.beam_search(c, alpha=0.3, beta=0.5, nbest=10) # any KenLM binary / ARPA file via lm=
return_nbest entries (beam order) hold the table words, the parts ctc, lmf, n and reranker (None
for "beam"), the decoder's score (ctc + alpha · lmf + beta · n for "beam"; the weighted sum above for
"rerank") and chosen. For "rerank", recombining the parts with decoding.json's weights reproduces the
decoder's choice; for "beam" the choice is the search's own first hypothesis, whose pruned search score can rank
differently from score.return_candidates gives, per frame, time (shifted back like word times), blank (its probability) and
words as (word, cosine, log p), best first; p is the softmax over the blank logit and s · cos with every row,
as the beam decoders' candidates. candidates_format="arrays" gives numpy arrays over every frame: time and
lp_blank [T], ids (indices into model.vocab), cos and logp [T, top_k], plus the fingerprint of the word
table they index.score_hypotheses(audio, hypotheses, decoder="rerank", include_nbest=True, lm=, alpha=, beta=, weights=) runs
one EmbeddingGemma 2 pass (inputs up to 40 s; long_form_s=None for longer ones) and scores each hypothesis, a
string or a list of table words, like an N-best entry. With include_nbest it returns the beam's own 32-best
entries unchanged, then each of your hypotheses scored as one more entry of that list, so yours change neither the
32-best scores nor each other; include_nbest=False scores only yours, as one list. Words outside the table are
listed in oov (never a candidate, read as a zero row by the reranker: add_words them first); chosen marks the
largest score.beam_search(candidates, lm=, alpha=, beta=, beam=, nbest=) is the beam decoders' search on
candidates_format="arrays" candidates (one dict or a list); on unchanged candidates of an input up to 40 s it
returns decoder="beam"'s N-best list. Candidates made before add_words / set_vocabulary are refused.profile="no_lm" works for score_hypotheses and beam_search too (no kenlm needed).| Corpus | Licence | Hours |
|---|---|---|
| LibriSpeech 960 h | CC BY 4.0 | 960 |
| Libri-light small + medium (our LibriSpeech-only reader's pseudo-labels) | public domain audio | 4,991 |
| The People's Speech, clean | CC BY 4.0 | 5,864 |
| VoxPopuli English, transcribed | CC0 1.0 | 415 |
| AMI IHM | CC BY 4.0 | 72 |
| Common Voice 17.0 English | CC0 1.0 | 1,575 |
| Multilingual LibriSpeech English (15k h subset) | CC BY 4.0 | 13,959 |
| Granary pseudo-labels: YODAS2 English (CC BY 3.0 videos) | CC BY 4.0 | 14,979 |
| Granary pseudo-labels: VoxPopuli unlabelled English | CC BY 4.0 (audio CC0) | 3,032 |
| Total | 45,847 |
Segments over 29 s were dropped. Transcripts are lower-cased and normalised to words; an utterance is kept only if every word is in the LibriSpeech vocabulary or occurs at least 3 times in these corpora. Sessions and videos that overlap evaluation sets were removed (Open ASR VoxPopuli test sessions; word 10-grams shared with evaluation references, e.g. YouTube re-uploads of GigaSpeech test talks).
Training: the reader was trained on LibriSpeech 960 h, then on the mix above (2 epochs on a 27.7k h subset, then 4 epochs on all of it), with EmbeddingGemma 2 frozen throughout. A 17,000-step partial run of the mix (about half an epoch) was the starting point of the final 4 epochs. The 4 epochs follow one cosine learning-rate schedule. Greedy mean WER of the eight sets at the start of epochs 3 and 4 and at the end: 7.35, 7.01 and 6.89; the last epoch still improved every set.
add_words.This project started from an idea we took from the Embedding Gemma Transcribe Mac app: reading EmbeddingGemma 2's audio as words of its own text space. We thank the authors of the projects below.
Readers that score EmbeddingGemma 2 text embeddings of words (as this model does):
| Project | Language | What it is |
|---|---|---|
| Embedding Gemma Transcribe | English | macOS app: frozen audio encoder and a trained reader that maps every audio slice to the closest of about 229,000 word embeddings, plus a punctuation model (weights not public) |
| mlboydaisuke/EmbeddingGemma-2-Transcribe-Reader (code, Core AI, LiteRT) | English | open reproduction: 19.6M reader, scale × cosine against 200,000 word rows plus a blank logit, CTC on LibriSpeech 960 h; iOS, macOS and Android apps |
| karim23657/fa-eg2-persian | Persian | the same reader with a Persian word table |
Other speech recognisers on the EmbeddingGemma 2 audio encoder (subword or character outputs):
| Project | Language | What it is |
|---|---|---|
| mstkyvz/embeddinggemma2-turkish-stt | Turkish | character CTC reader, LoRA adapter, 4-gram LM |
| kuchris/embeddinggemma2asr-cantonese (code) | Cantonese | frozen encoder, 20.8M CTC reader over characters and English words |
| Steveeeeeeen/embeddinggemma2-en-* | English | CTC, RNN-T, TDT and other heads trained on 1,000 h of YODAS / Granary |
| harshit2312/stt-gemma_embedding-2 | English | frozen encoder, 13M causal CTC head, subword 6-gram LM, Core ML |
| AigizK/EmbeddingGemma2-Bashkir-CV25 | Bashkir | Transformer CTC reader over Gemma tokens |
| tc3oliver/Embed2ASR-v1 (code) | English, Mandarin | frozen encoder, 9.66M GRU-CTC reader |
| MaorB/embeddinggemma-2-asr-hebrew-english-streaming | Hebrew, English | top encoder layers fine-tuned, streaming transducer over word pieces |
| 0xanto/eg2-tamil | Tamil | top 2 encoder layers fine-tuned, CTC over 1,024 BPE units |
| bringagi/gemma-stt-hindi | Hindi | top 2 encoder layers fine-tuned, character CTC, word 3-gram LM |
Code: Apache 2.0 (LICENSE-CODE). Model files: Apache License 2.0 (LICENSE). EmbeddingGemma 2 keeps its own terms
and the Gemma Prohibited Use Policy; the n-gram LM (lee1jun/LES-eg2-asr-en-lm-4gram) is CC BY 4.0. Attributions: NOTICE.
If you use this model, please cite it:
@misc{les_eg2_665m_asr_en,
title = "{LES-eg2-665M-asr-en}: English Speech Recognition from a Frozen {EmbeddingGemma 2} with a Word-Embedding {CTC} Reader",
author = {lee1jun},
year = {2026},
month = nov,
publisher = {Hugging Face},
howpublished = {\url{https://huggingface.co/lee1jun/LES-eg2-665M-asr-en}},
note = {n-gram LM: \url{https://huggingface.co/lee1jun/LES-eg2-asr-en-lm-4gram}}
}
This model builds on EmbeddingGemma 2:
@article{embeddinggemma2,
title = {EmbeddingGemma 2: Unified and Efficient Multimodal Representations},
author = {Schechter Vera, Henrique and Dua, Sahil and others},
journal = {arXiv preprint arXiv:2610.37364},
year = {2026}
}
| File | Size |
|---|---|
LICENSE |
0.01 MiB |
LICENSE-CODE |
0.01 MiB |
NOTICE |
0.00 MiB |
PROVENANCE.json |
0.00 MiB |
README.md |
0.03 MiB |
architecture.png |
0.24 MiB |
architecture.svg |
0.01 MiB |
config.json |
0.00 MiB |
config.yaml |
0.00 MiB |
configuration_eg2_word_ctc.py |
0.00 MiB |
decoding.json |
0.00 MiB |
embedding_space.png |
0.35 MiB |
embedding_space.svg |
0.07 MiB |
model.safetensors |
335.79 MiB |
modeling_eg2_word_ctc.py |
0.07 MiB |
nbest_paths.png |
0.18 MiB |
nbest_paths.svg |
0.01 MiB |
requirements.txt |
0.00 MiB |
reranker_heads.safetensors |
24.58 MiB |
reranker_heads_nolm.safetensors |
24.58 MiB |
secondpass_eg2.py |
0.03 MiB |
seen_vocab.json |
4.30 MiB |
vocab.json |
4.54 MiB |
word_table.safetensors |
300.19 MiB |
lee1jun (contact: lee1jun@kaist.ac.kr)