Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,26 @@ npm install clap-cpp
## Usage

```js
const { Model, decodeAudio } = require('clap-cpp');
const { CLAP } = require('clap-cpp');

const model = new Model('/path/to/model.gguf', 4);
const model = new CLAP('/path/to/model.gguf', 4);
try {
console.log(model.projectionDimension, model.audioSampleRate);
const text = model.text('a piano playing'); // owned Float32Array
const { samples, sampleRate } = decodeAudio('/path/to/audio.wav');
const audio = model.audio(samples, sampleRate); // automatically resampled
const text = model.encodeText('a piano playing'); // owned Float32Array
const audio = model.encodeAudio('/path/to/audio.wav');
// CLAP projections are unit-normalized, so their dot product is cosine.
const similarity = text.reduce((sum, x, i) => sum + x * audio[i], 0);
console.log(similarity);
} finally {
model.close(); // optional early release; garbage collection also frees weights
}
```

The package exports only `CLAP`. Its inference API consists of:

- `encodeText(textOrTokens)`: accepts a string, `Int32Array` or `number[]` token IDs.
- `encodeAudio(path, cropOffset = 0)`: decodes WAV/MP3/FLAC by contents, downmixes to mono and automatically resamples for the model.
- `encodeAudio(pcm, sampleRate = 48000, cropOffset = 0)`: accepts mono `Float32Array` samples, with automatic resampling.

Both methods return owned `Float32Array` embeddings. Crop offsets are measured in samples; support depends on the model architecture. `close()` is idempotent; encoding after closing throws.

Inference is synchronous and blocks the calling JavaScript thread. Use a separate model per worker for parallel execution. Tokenization, audio decoding, resampling, frame features and model metadata are not separate public APIs.
90 changes: 19 additions & 71 deletions node/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,29 +74,16 @@ Napi::Float32Array array(Napi::Env env, const std::vector<float> &v) {
if (!v.empty()) std::memcpy(output.Data(), v.data(), v.size() * sizeof(float));
return output;
}
Napi::Object frames(Napi::Env env, const clap::AudioFrames &f) {
auto output = Napi::Object::New(env);
output.Set("values", array(env, f.values));
output.Set("frames", Napi::Number::New(env, f.frames));
output.Set("dimension", Napi::Number::New(env, f.dimension));
return output;
}

class Model : public Napi::ObjectWrap<Model> {
class CLAP : public Napi::ObjectWrap<CLAP> {
public:
static Napi::Function define(Napi::Env env) {
return DefineClass(env, "Model", {
InstanceAccessor<&Model::projection_dimension>("projectionDimension"),
InstanceAccessor<&Model::audio_sample_rate>("audioSampleRate"),
InstanceMethod<&Model::tokenize>("tokenize"),
InstanceMethod<&Model::text>("text"),
InstanceMethod<&Model::audio>("audio"),
InstanceMethod<&Model::audio_frames>("audioFrames"),
InstanceMethod<&Model::clamp3_features>("clamp3Features"),
InstanceMethod<&Model::close>("close")
return DefineClass(env, "CLAP", {
InstanceMethod<&CLAP::encode_text>("encodeText"),
InstanceMethod<&CLAP::encode_audio>("encodeAudio"),
InstanceMethod<&CLAP::close>("close")
});
}
explicit Model(const Napi::CallbackInfo &info) : Napi::ObjectWrap<Model>(info) {
explicit CLAP(const Napi::CallbackInfo &info) : Napi::ObjectWrap<CLAP>(info) {
guarded(info.Env(), [&]() -> Napi::Value {
auto p = path(info[0]);
int threads = info[1].IsUndefined() ? 0 : static_cast<int>(integer(info[1], "threads", 0, std::numeric_limits<int>::max()));
Expand All @@ -109,27 +96,10 @@ class Model : public Napi::ObjectWrap<Model> {
// own ObjectWrap. There is no globally shared model handle or borrowed output.
std::unique_ptr<clap::CLAP> model_;
clap::CLAP &get() {
if (!model_) throw std::runtime_error("Model is closed");
if (!model_) throw std::runtime_error("CLAP is closed");
return *model_;
}
Napi::Value projection_dimension(const Napi::CallbackInfo &info) {
return guarded(info.Env(), [&]() -> Napi::Value { return Napi::Number::New(info.Env(), get().projection_dimension()); });
}
Napi::Value audio_sample_rate(const Napi::CallbackInfo &info) {
return guarded(info.Env(), [&]() -> Napi::Value { return Napi::Number::New(info.Env(), get().audio_sample_rate()); });
}
Napi::Value tokenize(const Napi::CallbackInfo &info) {
return guarded(info.Env(), [&]() -> Napi::Value {
auto &m = get();
auto s = string(info[0], "text");
auto n = info[1].IsUndefined() ? 512 : integer(info[1], "maxLength", 1, std::numeric_limits<int>::max());
auto ids = m.tokenize(s, n);
auto out = Napi::Int32Array::New(info.Env(), ids.size());
for (std::size_t i = 0; i < ids.size(); ++i) out[i] = ids[i];
return out;
});
}
Napi::Value text(const Napi::CallbackInfo &info) {
Napi::Value encode_text(const Napi::CallbackInfo &info) {
return guarded(info.Env(), [&]() -> Napi::Value {
// Array element getters can reenter JS and close this model. Parse
// completely before taking a native reference, then recheck ownership.
Expand All @@ -141,28 +111,21 @@ class Model : public Napi::ObjectWrap<Model> {
return array(info.Env(), get().encode_text(ids));
});
}
Napi::Value audio(const Napi::CallbackInfo &info) {
Napi::Value encode_audio(const Napi::CallbackInfo &info) {
return guarded(info.Env(), [&]() -> Napi::Value {
auto &m = get();
if (info[0].IsString()) {
const auto filename = path(info[0]);
const auto crop = info[1].IsUndefined() ? 0 : integer(info[1], "cropOffset", 0,
std::min(9007199254740991.0, static_cast<double>(std::numeric_limits<std::size_t>::max())));
auto &m = get();
auto decoded = clap::decode_audio(filename);
return array(info.Env(), m.encode_audio(decoded.samples, decoded.sample_rate, crop));
}
auto pcm = samples(info[0]);
int sr = optional_rate(info[1], 48000);
auto crop = info[2].IsUndefined() ? 0 : integer(info[2], "cropOffset", 0,
std::min(9007199254740991.0, static_cast<double>(std::numeric_limits<std::size_t>::max())));
return array(info.Env(), m.encode_audio(pcm, sr, crop));
});
}
Napi::Value audio_frames(const Napi::CallbackInfo &info) {
return guarded(info.Env(), [&]() -> Napi::Value {
auto &m = get();
auto pcm = samples(info[0]);
return frames(info.Env(), m.audio_frames(pcm, optional_rate(info[1], 48000)));
});
}
Napi::Value clamp3_features(const Napi::CallbackInfo &info) {
return guarded(info.Env(), [&]() -> Napi::Value {
auto &m = get();
auto pcm = samples(info[0]);
return frames(info.Env(), m.clamp3_features(pcm, optional_rate(info[1], 24000)));
return array(info.Env(), get().encode_audio(pcm, sr, crop));
});
}
Napi::Value close(const Napi::CallbackInfo &info) {
Expand All @@ -171,22 +134,7 @@ class Model : public Napi::ObjectWrap<Model> {
};

Napi::Object initialize(Napi::Env env, Napi::Object exports) {
exports.Set("Model", Model::define(env));
exports.Set("decodeAudio", Napi::Function::New(env, [](const Napi::CallbackInfo &info) {
return guarded(info.Env(), [&]() -> Napi::Value {
auto decoded = clap::decode_audio(path(info[0]));
auto result = Napi::Object::New(info.Env());
result.Set("samples", array(info.Env(), decoded.samples));
result.Set("sampleRate", Napi::Number::New(info.Env(), decoded.sample_rate));
return result;
});
}, "decodeAudio"));
exports.Set("resampleAudio", Napi::Function::New(env, [](const Napi::CallbackInfo &info) {
return guarded(info.Env(), [&]() -> Napi::Value {
clap::Audio input{samples(info[0]), rate(info[1])};
return array(info.Env(), clap::resample_audio(input, optional_rate(info[2], 48000)).samples);
});
}, "resampleAudio"));
exports.Set("CLAP", CLAP::define(env));
return exports;
}
} // namespace
Expand Down
35 changes: 8 additions & 27 deletions node/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,13 @@
/** Owned, flat time-major values: values.length === frames * dimension. */
export interface AudioFrames {
values: Float32Array;
frames: number;
dimension: number;
}
export interface Audio {
samples: Float32Array;
sampleRate: number;
}
/** Synchronous CPU inference. Each call blocks the calling JavaScript thread. */
export class Model {
export class CLAP {
/** threads=0 selects the native default; negative/fractional values are rejected. */
constructor(path: string, threads?: number);
readonly projectionDimension: number;
readonly audioSampleRate: number;
/** Native tokenizer; maxLength defaults to 512. Returns owned token IDs. */
tokenize(text: string, maxLength?: number): Int32Array;
text(input: string | Int32Array | number[]): Float32Array;
/** Mono PCM. sampleRate defaults to 48000; cropOffset defaults to 0 samples. */
audio(pcm: Float32Array, sampleRate?: number, cropOffset?: number): Float32Array;
/** Available only for architectures exposing frame encodings. Default: 48000 Hz. */
audioFrames(pcm: Float32Array, sampleRate?: number): AudioFrames;
/** MERT-v1 all-layer mean features for CLaMP3. Default: 24000 Hz. */
clamp3Features(pcm: Float32Array, sampleRate?: number): AudioFrames;
/** Idempotent. All other methods/properties throw after close. */
/** Returns an owned embedding. Text is tokenized internally. */
encodeText(input: string | Int32Array | number[]): Float32Array;
/** Mono PCM, automatically resampled. Defaults: 48000 Hz, cropOffset=0 samples. */
encodeAudio(pcm: Float32Array, sampleRate?: number, cropOffset?: number): Float32Array;
/** Decode WAV/MP3/FLAC by contents, downmix and resample internally. */
encodeAudio(path: string, cropOffset?: number): Float32Array;
/** Idempotent early release; encoding methods throw after close. */
close(): void;
}
/** WAV/MP3/FLAC decoded by contents, downmixed to mono, original sample rate. */
export function decodeAudio(path: string): Audio;
/** Owned output. targetRate defaults to 48000; both rates must be positive integers. */
export function resampleAudio(pcm: Float32Array, sourceRate: number, targetRate?: number): Float32Array;
4 changes: 2 additions & 2 deletions node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
"main": "index.js",
"types": "index.d.ts",
"engines": { "node": ">=22.0.0" },
"files": ["index.js", "index.d.ts", "bindings.cpp", "CMakeLists.txt", "scripts", "native", "native-tests", "test/addon.test.js", "test/packaging.test.js", "test/generate-fixtures.py", "test/types.ts", "test/tsconfig.json", "README.md", "LICENSE", "THIRD_PARTY_LICENSES"],
"files": ["index.js", "index.d.ts", "bindings.cpp", "CMakeLists.txt", "scripts", "native", "native-tests", "test/addon.test.js", "test/surface.test.js", "test/packaging.test.js", "test/generate-fixtures.py", "test/types.ts", "test/tsconfig.json", "README.md", "LICENSE", "THIRD_PARTY_LICENSES"],
"scripts": {
"install": "cmake-js compile",
"build": "cmake-js compile",
"rebuild": "cmake-js rebuild",
"test:fixtures": "python3 test/generate-fixtures.py",
"test": "node --test test/addon.test.js",
"test": "node --test test/addon.test.js test/surface.test.js",
"test:packaging": "node --test test/packaging.test.js",
"test:types": "tsc -p test/tsconfig.json",
"prepack": "node scripts/vendor.js"
Expand Down
Loading
Loading