Skip to content

EmbeddingGemma 2 audio: index an hour of audio in ~5 s, search it with text - #31

Merged
Alex-Wengg merged 10 commits into
mainfrom
feat/embeddinggemma2-audio
Oct 9, 2026
Merged

Alex-Wengg merged 10 commits into
mainfrom
feat/embeddinggemma2-audio

Conversation

@Alex-Wengg

Copy link
Copy Markdown
Member

Adds EmbeddingGemma 2's audio encoder (Core ML, from FluidInference/embeddinggemma-2-coreml @ c0fefd0a) and a search-audio demo.

  • EmbeddingGemma2Audio: 10 s windows → audio encoder on the GPU (the ANE falls back to CPU on the chunked attention) → text model on the ANE; audio and text share one space. 1 hour indexed in 4.6–5.7 s (626–777× real time); 4 h 36 min over 2,301 files in 34.7 s.
  • Parity: Swift end to end vs sentence-transformers fp32 cos 0.9995; fp16 vs fp32 top hit identical on 15/15 queries over a 1-hour index.
  • AudioSearchCheck (CLI) and AudioSearchDemo (SwiftUI + demo.sh with macmon and live log); default audio = public datasets already on disk, nothing bundled.
  • Store: audio is its own pinned bundle, so the text model is not recompiled.

Quality note: speech queries work well; sound-event search is weak in the model itself (ESC-50 zero-shot 24%, identical in PyTorch).

Checked locally: release build, swift-format lint, AudioSearchCheck on the Earnings-22 hour / ESC-50 / LibriSpeech; unit tests for the window arithmetic run in CI.

🤖 Generated with Claude Code

…th text

EmbeddingGemma2Audio runs the model's audio encoder (Gemma 4 USM
conformer, 12 layers, log-mel computed in-graph) from the new
EmbeddingGemma2Audio.mlpackage on FluidInference/embeddinggemma-2-coreml
(revision c0fefd0a, pinned + checksummed; the text bundle keeps its own
revision and compile stamp, so nothing recompiles). Each 10 s window of
16 kHz audio becomes 250 tokens, which the text model embeds as
<bos> <|audio> tokens <audio|> <eos> through embed_256 — audio lands in the
text space and is searchable with a text query.

- Audio encoder on the GPU: the chunked local attention's 5-D blocks fall
  back to the CPU on the ANE (46 ms per window vs 13.6 ms on the GPU).
  Windows run four in flight across all files, so the GPU (audio) and the
  Neural Engine (text) overlap: 1 hour in 4.6-5.7 s (626-777x real time),
  4 h 36 min across 2,301 files in 34.7 s (478x).
- fp16 drifts on a few quiet tokens; on a 1-hour index the top hit matched
  fp32 for 15/15 queries (top-5 overlap 95%). Swift end to end vs
  sentence-transformers fp32: cos 0.9995 on a LibriSpeech clip.
- Any AVFoundation format decoded to 16 kHz mono; window/frame arithmetic
  matches the HF feature extractor (999 frames / 250 tokens per 10 s,
  347 / 87 for 3.48 s), covered by unit tests.

AudioSearchCheck: index files or folders, timing, --query, --dump.
AudioSearchDemo: index the public datasets on this Mac (Earnings-22 hour,
FLEURS en/fr, ESC-50) or --audio= folders, live speed tiles, search as you
type (one sgemv over all windows), play the matching 10 s; demo.sh adds
macmon + the live log.

Speech queries work well; sound events are weak in the model itself
(ESC-50 zero-shot 24% over 50 classes, same as PyTorch fp32).
…uery, play its top 3

Stop/Auto (Space) toggles it; typing a query or pressing play ends it; --manual starts
without it, --clip= sets seconds per result. Search now ranks through one shared path
(rank) for typed and hands-free queries.
… s of search

Hands-free now alternates two segments (--segment=, default 30 s):
- Speed: read and embed every file again as fast as it can, with live
  speed tiles. The first pass runs to completion and is searchable as it
  grows (windows stream in through EmbeddingGemma2Audio's new onWindow
  callback); later passes build a new index beside the current one and
  are cut at the segment end, so searches never see a partial index.
- Search: type each suggested query and play its top three (4 s each,
  --clip=), continuing through the suggestion list across segments.

Fixes found on the way: the field's change callback echoing a query the
show typed (or cleared) switched hands-free off; the speed countdown
returned before the indexing task had started, so a timed pass ran to
the end. EmbeddingGemma2Audio checks for cancellation after each window.
… loop

After the first read, the show alternates 30 s of listening (type a query,
play its top three) with 30 s of search speed: ~100 different queries
(suggestions, earnings/news topics, the ESC-50 sound classes) back to back,
64 at a time, embedded eight per Neural Engine call (pack_256) and ranked
against every window with one sgemm; each query gets its own top 10, the
latest is shown. Tiles switch to searches run, searches/s (rolling 1 s),
ms per search and windows ranked per second. ~700 searches/s, 1.4 ms each
(embed 1.4 ms, rank 0.02 ms) over 2,786 windows. The packed path is warmed
at load so the first second is not a cold start. The timed re-read
segments are gone.
Indexing starts at launch with its own tile row; the listen / search-speed show starts as soon
as 64 windows are searchable and searches whatever is indexed by then. Search speed waits for
indexing to finish (both use the Neural Engine; during indexing it measured 75/s instead of ~700/s),
so the first listening segment runs until then. Search tiles get their own row.
The show waits until every file is indexed (status: indexing · search starts when done), then
alternates listen and search speed.
…n stop

Ends with a summary line (audio indexed and its speed, searches run at their average rate);
Auto (Space) runs it again.
Three buttons replace Index / Auto / Reset. Play (↩) runs the show once:
index every file, 30 s of listening (type each suggested query, play its
top three), 30 s of search speed, done with a summary. Pause (Space)
holds it anywhere: indexing (EmbeddingGemma2Audio.embed gains a
beforeWindow hook used as a pause gate), typing, the playing clip, the
speed loop, and the step timers and speeds, which count running time
only. Replay (⌘R) clears the index and runs it again. Nothing starts on
launch (--autostart presses Play); the search field, suggestions and row
play buttons work when the show is not running.
forward-looking statements disclaimer, a baby crying, the operator opens the line for questions,
someone laughing (two earnings-call phrases, two sounds; all four return three right hits). Clips
are 3 s; the step is no longer cut at 30 s.
…e names

The listening step plays the top 3 of eight searches: two earnings-call
phrases (forward-looking statements, the operator opening the line) and
six sounds (baby crying, church bells, siren, laughing, cow, birds). With
the ESC-50 clips renamed to random ids (clip_3f9a1c.wav; classes kept in
esc50/labels.json, outside the app's view) all 24 hits are right, so the
file name plays no part; the header says so. Search-speed queries for
the sound classes come from labels.json. Search speed stays 30 s
(21,312 searches at 710/s over 2,786 windows).
@Alex-Wengg
Alex-Wengg merged commit c7e9f0e into main Oct 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant