CLAP HTSAT-fused — ExecuTorch (search sound with words)

CLIP for audio. An audio tower and a text tower put a clip and a phrase in the same 512-dimensional space, so "a dog barking" can be matched against a recording with a cosine — with words the app chooses at runtime, not a label list fixed at training time.

The shelf already has AST, which answers with AudioSet's 527 labels, and CLIP and SigLIP2, which do this for pictures. This is the piece that was missing: a searchable representation of sound.

audio: input_features (1,4,1001,64) -> (1,512)
text : (input_ids (1,32), attention_mask (1,32)) -> (1,512)

Both embeddings come out L2-normalised, so a cosine is a dot product. Two files because the text side runs once per phrase and can be cached; the audio side runs once per clip.

tower build file MB embedding cosine vs eager Mac ms*
audio XNNPACK fp32 clap_audio_xnnpack_fp32.pte 113.1 1.000000 115.5
audio XNNPACK int8 clap_audio_xnnpack_int8.pte 29.1 0.999613 115.2
text XNNPACK fp32 clap_text_xnnpack_fp32.pte 501.4 1.000000 11.6
text XNNPACK int8 clap_text_xnnpack_int8.pte 243.2 0.990783 12.4
text Core ML (iOS) clap_text_coreml_all.pte 251.2 0.999999 2.5

The int8 pair is 272 MB against fp32's 615 MB, and both keep all five winning phrases. The audio tower quantises well (0.9996); the text tower is the one to watch at 0.9908, which clears this shelf's 0.99 bar for embeddings but not by much.

*Mac arm64, single process, median of 5 — a reference point for relative cost, not a device number. Torch eager fp32 on the same machine: audio 29.0 ms, text 13.9 ms. XNNPACK delegate coverage: audio 64.2%, text 77.3%.

The text tower is the big one — it is a RoBERTa base, 124.65 M parameters against the audio tower's 27.55 M — and it is also the one you can run once and keep. A phrase set of twenty labels is twenty 512-float vectors.

The audio tower has no Core ML build. coremltools stops on a data-dependent size inside the HTSAT windowing (Could not extract specialized integer from data-dependent expression u12). The text tower converts cleanly and is 100% delegated there.

Running it

1. The audio. ClapFeatureExtractor at 48 kHz mono: 64 mel bins, a 10-second window, and the four-chunk fusion layout the -fused checkpoint wants — input_features comes out [1, 4, 1001, 64]. Feed that straight in.

The extractor also returns is_longer, and this graph does not take it. That is deliberate, and it is the one thing worth reading before you use this file.

2. The phrase. The repo's RoBERTa tokenizer with padding="max_length", max_length=32. Padding length does not change the embedding — checked, cosine 1.0 against padding to the natural length — but the graph's shape is fixed at 32.

3. The match. Dot the two vectors. Both are already unit length, so the dot product is the cosine. For a small label set, embed every phrase once, stack them, and take the argmax.

The is_longer flag, and why it is not an input

ClapFeatureExtractor ends with this:

if truncation == "fusion" and sum(is_longer) == 0:
    # if no audio is longer than 10s, then randomly select one audio to be longer
    rand_idx = np.random.randint(0, len(input_mel))
    is_longer[rand_idx] = True

For a batch of one — what an app sends — that means is_longer is always True, however short the clip. Measured on five macOS system sounds of 0.76 to 1.65 seconds: True every time.

So the fusion path is the real path, and the graph takes it unconditionally. The alternative was to keep the if len(is_longer_idx) > 0 branch, which torch.export cannot specialise (GuardOnDataDependentSymNode ... u0) because it is a Python branch on the size of a torch.where result.

Building the other half — global-only, is_longer forced False — moves the embedding to cosine 0.78–0.87 against the stock model and changes which phrase wins. That build is wrong, and the way it first looked right is worth recording: it was compared against a copy of itself that had the same forced flag, and reported max_abs_diff 0.000e+00. The control has to move the variable under test.

The branch-free fusion graph matches the stock module driven by the extractor's own flag at max_abs_diff 0.000e+00.

What is measured

Two numbers, because an embedding model has two things that can go wrong.

Fidelity. Cosine between each build's embedding and the fp32 model's, over five sounds and six phrases: 1.000000 for both fp32 towers, and 0.999613 (audio) / 0.990783 (text) for int8.

Behaviour. Whether the phrase that wins for each sound is still the same: 5/5 for fp32 and for int8.

Ping        an electronic beep
Submarine   an electronic beep
Purr        a bubble popping
Sosumi      an electronic beep
Glass       a bell ringing
python convert/check_clap.py fp32     # or int8
python convert/audit_int8.py clap_audio

The audio is macOS's own system sounds — short real recordings. That matters: the first probe used synthetic speech from say, and the fp32 model rated it "rain falling" at 0.82 before any conversion was involved. CLAP is trained on environmental audio and speech is not what it is for. Five sounds and six phrases is a conversion check, not an evaluation of the model.

torch.export -> to_edge_transform_and_lower(partitioner) -> .pte (conversion scripts: executorch-models)

Downloads last month
-
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for mlboydaisuke/CLAP-HTSAT-fused-ExecuTorch

Quantized
(1)
this model