diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..786ff20 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,39 @@ +name: Rust + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + cpu: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + env: + CARGO_BUILD_JOBS: 4 + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install stable Rust and checks + run: rustup toolchain install stable --profile minimal --component rustfmt --component clippy + - name: Format + run: cargo +stable fmt --manifest-path rust/Cargo.toml -- --check + - name: Lint + run: cargo +stable clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings + - name: Real CPU inference and safety tests + run: cargo +stable test --manifest-path rust/Cargo.toml --all-targets + - name: Compile-fail thread-safety contract + run: cargo +stable test --manifest-path rust/Cargo.toml --doc + # This is intentionally a path-only crate; inventory is not a package build. + - name: Inspect non-publishable package inventory + run: cargo +stable package --manifest-path rust/Cargo.toml --list --allow-dirty diff --git a/README.md b/README.md index 374b355..1e04641 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,15 @@ NumPy waveform input and output, audio decoding/resampling, and model lifetime management. It builds a self-contained CPU extension; no PyTorch or GPU runtime is needed for inference. See the package README for API examples and tests. +## Rust bindings (CPU) + +The [`rust/` crate](rust/README.md) provides safe model ownership, text/audio +embeddings, frame/features APIs, decoding, resampling and log-mel preprocessing. +Use it as a local path dependency from an initialized recursive checkout; it is +intentionally not publishable until native-source vendoring is implemented. +Run `cargo test --manifest-path rust/Cargo.toml -j 4` (Python 3 is needed only to +generate tiny test fixtures). Inference requires no Python or GPU runtime. + ## Build and test ```sh diff --git a/rust/.gitignore b/rust/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/rust/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 0000000..6a2514b --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "clap-cpp" +version = "0.1.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..e6cb877 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "clap-cpp" +version = "0.1.0" +edition = "2021" +description = "Safe CPU Rust bindings for clap.cpp" +license = "Apache-2.0" +repository = "https://github.com/gorse-io/clap.cpp" +readme = "README.md" +# Native sources are in the parent checkout and Git submodules, not this crate. +publish = false + +[lib] +name = "clap_cpp" +path = "src/lib.rs" diff --git a/rust/README.md b/rust/README.md new file mode 100644 index 0000000..264e98e --- /dev/null +++ b/rust/README.md @@ -0,0 +1,109 @@ +# Rust bindings + +`clap-cpp` provides safe, owning CPU bindings to the current C++ `clap::Model` +API. It does **not** depend on the proposed public C API or a system installation +of clap.cpp. Rust has no third-party crate dependencies. + +## Build and use + +Requirements: Rust stable, CMake >= 3.20, a C/C++17 compiler, and a complete +repository checkout with these pinned submodules initialized: + +```sh +git submodule update --init --recursive ggml thirdparty/json thirdparty/icu thirdparty/miniaudio +cargo test --manifest-path rust/Cargo.toml -j 4 +cargo run --manifest-path rust/Cargo.toml --example embed -- model.gguf "a piano" audio.wav +``` + +Python 3 (stdlib only) is needed **only for integration tests**, which generate +tiny valid GGUF models and WAV data without downloading weights. Windows uses +the MSVC Rust toolchain and Visual Studio C++ build tools; macOS uses the Xcode +command-line tools. Native cross-compilation is not currently supported. Choose +moderate Cargo job counts to bound CMake compilation parallelism. + +In another local project: + +```toml +[dependencies] +clap-cpp = { path = "/path/to/clap.cpp/rust" } +``` + +```rust,no_run +use clap_cpp::{Model, decode_audio}; +# fn main() -> clap_cpp::Result<()> { +let mut model = Model::new("model.gguf", 4)?; +let tokens = model.tokenize("a piano", 512)?; +let embedding = model.text_tokens(&tokens)?; +let audio = decode_audio("piano.wav")?; +let embedding = model.audio(&audio.samples, audio.sample_rate, 0)?; +# Ok(()) +# } +``` + +The independent CMake entry point builds only core, CPU ggml, bundled ICU/json, +and the private bridge. Libraries are static and PIC; GPU backends, host-native +ISA tuning, BLAS/Accelerate, OpenMP, CLI tools and native tests are disabled. +System C/C++ runtimes are still required; no clap/ggml/ICU shared libraries or +ICU data files are needed. CMake's usual `CC`, `CXX`, `CFLAGS`, `CXXFLAGS`, +`CMAKE_GENERATOR` and `CMAKE_TOOLCHAIN_FILE` settings can configure native builds. + +## API and safety + +- `Model::new(path, threads)` loads one GGUF; `Drop` releases it. Zero threads + selects the native default. Negative thread counts are rejected. +- Models are deliberately **neither `Send` nor `Sync`**. Inference takes + `&mut self`; do not bypass this with unsafe code. Backend same-instance + concurrency is not guaranteed. +- `tokenize`, `text`, `text_tokens`, `audio`, `audio_frames`, + `clamp3_features`, `audio_sample_rate`, `projection_dimension` mirror native + operations. Unsupported architecture/operation combinations return errors. + `tokenize` truncates to a maximum, but does **not** pad. String inference uses + the native architecture's context policy. Empty text can be valid; empty + token slices are not. +- Frame results are time-major `AudioFrames { values, frames, dimension }`. + `values.len() == frames * dimension`. MERT final frames and all-layer CLaMP3 + preprocessing features are different operations; CLAP does not expose either. +- `decode_audio` supports WAV/MP3/FLAC and averages channels, preserving source + rate. `resample_audio` uses the native causal linear/anti-alias resampler; + its output is not zero-phase sinc. `log_mel` accepts 48 kHz mono PCM and + returns flattened `[1001, 64]` features with native repeat-padding/cropping. + `audio` performs model-specific resampling; crop offsets retain the native + model's meaning (CLAP: samples after resampling to 48 kHz). Other architectures + may reject nonzero crop offsets. +- Native exceptions are caught inside the private bridge. Every result has a + paired C++ free; output vectors and diagnostics are copied into Rust-owned + values before release, never borrowed from a destroyed native object. An + allocation failure while creating a native result becomes an `Error`; native + diagnostic text is bounded to 1023 bytes. Rust allocation failure retains + Rust's standard process-abort behavior. +- Strings and paths must be UTF-8 and cannot contain NUL. Non-UTF-8 OS paths + are rejected rather than lossy-converted. Input slices are borrowed only for + synchronous calls. PCM must be nonempty and finite with positive sample rate; + invalid tokens and architecture/context constraints are checked natively. + +## Packaging limitation (intentional) + +This is a **repository/path dependency**, not a standalone crates.io package. +`publish = false` prevents publishing an unusable source archive. Cargo packages +only files inside `rust/`; the parent C++ sources and Git submodules are therefore +absent from `cargo package` archives. A package archive cannot build standalone, +and a plain Cargo Git dependency does not reliably initialize the required +submodules. Use an explicitly cloned, initialized checkout and a path dependency. +Do not remove the publishing guard without first adding a reviewed native-source +vendoring/release process, all dependency licenses, and an isolated package-build +test. `cargo package --list --allow-dirty` can inspect the archive inventory; +that is not evidence of standalone package viability. + +## Checks + +```sh +cargo fmt --manifest-path rust/Cargo.toml -- --check +cargo clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings +cargo test --manifest-path rust/Cargo.toml +``` + +Tests exercise actual CLAP text/waveform and MERT inference, independent scalar +text values, dimensions, frame mean-pooling, decoding/resampling/mel values, +UTF-8 paths, owned errors, unsupported operations, invalid IDs and nonfinite PCM. +Compile-fail doctests enforce the !Send/!Sync contract. Large published models, +GPU inference and exhaustive numerical parity are outside this binding suite. diff --git a/rust/build.rs b/rust/build.rs new file mode 100644 index 0000000..d25a230 --- /dev/null +++ b/rust/build.rs @@ -0,0 +1,108 @@ +use std::{env, path::PathBuf, process::Command}; + +fn run(command: &mut Command) { + let status = command + .status() + .expect("cannot run CMake; install CMake >=3.20 and a C++17 compiler"); + assert!(status.success(), "native CMake build failed: {command:?}"); +} + +fn main() { + let manifest = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let root = manifest.parent().unwrap(); + assert!(root.join("include/clap.h").is_file(), + "clap-cpp requires the parent repository native sources; use a path dependency from a recursive Git checkout, not a cargo package archive (see rust/README.md)"); + for path in [ + "rust/native", + "src", + "include", + "cmake", + "ggml", + "thirdparty/json", + "thirdparty/icu", + "thirdparty/miniaudio", + ] { + println!("cargo:rerun-if-changed={}", root.join(path).display()); + } + for var in [ + "CMAKE_GENERATOR", + "CMAKE_TOOLCHAIN_FILE", + "CC", + "CXX", + "CFLAGS", + "CXXFLAGS", + ] { + println!("cargo:rerun-if-env-changed={var}"); + } + let target = env::var("TARGET").unwrap(); + assert_eq!( + env::var("HOST").unwrap(), + target, + "cross-compilation is not supported yet" + ); + let out = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + let build = out.join("build"); + let install = out.join("native"); + let mut configure = Command::new("cmake"); + configure + .arg("-S") + .arg(manifest.join("native")) + .arg("-B") + .arg(&build) + .arg("-DCMAKE_BUILD_TYPE=Release") + .arg(format!("-DCMAKE_INSTALL_PREFIX={}", install.display())); + if let Some(toolchain) = env::var_os("CMAKE_TOOLCHAIN_FILE") { + configure.arg(format!( + "-DCMAKE_TOOLCHAIN_FILE={}", + toolchain.to_string_lossy() + )); + } + run(&mut configure); + run(Command::new("cmake") + .arg("--build") + .arg(&build) + .args([ + "--config", + "Release", + "--target", + "clap-rust-native", + "--parallel", + ]) + .arg(env::var("NUM_JOBS").unwrap_or_else(|_| "2".into()))); + run(Command::new("cmake").arg("--install").arg(&build).args([ + "--config", + "Release", + "--component", + "rust", + ])); + println!( + "cargo:rustc-link-search=native={}", + install.join("lib").display() + ); + for lib in [ + "clap-rust-native", + "clap", + "ggml", + "ggml-cpu", + "ggml-base", + "clap-icu-i18n", + "clap-icu-uc", + "clap-icu-data", + ] { + println!("cargo:rustc-link-lib=static={lib}"); + } + if target.contains("apple") { + println!("cargo:rustc-link-lib=c++"); + } else if target.contains("windows") { + assert!( + target.contains("msvc"), + "Windows requires the MSVC Rust toolchain" + ); + // Static ggml CPU detection and ICU timezone support use registry APIs. + println!("cargo:rustc-link-lib=advapi32"); + } else { + for lib in ["stdc++", "pthread", "m", "dl"] { + println!("cargo:rustc-link-lib={lib}"); + } + } +} diff --git a/rust/examples/embed.rs b/rust/examples/embed.rs new file mode 100644 index 0000000..6033cb0 --- /dev/null +++ b/rust/examples/embed.rs @@ -0,0 +1,20 @@ +use clap_cpp::{decode_audio, Model}; + +fn main() -> Result<(), Box> { + let mut args = std::env::args().skip(1); + let path = args + .next() + .ok_or("usage: cargo run --example embed -- MODEL.gguf TEXT [AUDIO]")?; + let text = args.next().ok_or("missing text")?; + let mut model = Model::new(path, 0)?; + println!("projection dimension: {}", model.projection_dimension()?); + println!("text: {:?}", model.text(&text)?); + if let Some(path) = args.next() { + let audio = decode_audio(path)?; + println!( + "audio: {:?}", + model.audio(&audio.samples, audio.sample_rate, 0)? + ); + } + Ok(()) +} diff --git a/rust/native/CMakeLists.txt b/rust/native/CMakeLists.txt new file mode 100644 index 0000000..6205ba4 --- /dev/null +++ b/rust/native/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.20) +project(clap_cpp_rust VERSION 0.1.0 LANGUAGES C CXX) + +get_filename_component(CLAP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE) +foreach(source ggml/CMakeLists.txt thirdparty/json/CMakeLists.txt thirdparty/miniaudio/miniaudio.h) + if(NOT EXISTS "${CLAP_ROOT}/${source}") + message(FATAL_ERROR "Native sources missing: run git submodule update --init --recursive") + endif() +endforeach() + +# Build a self-contained CPU extension, never CLI executables, GPU backends, +# external OpenMP runtimes, or shared libraries requiring a loader search path. +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) +set(GGML_NATIVE OFF CACHE BOOL "" FORCE) +set(GGML_CPU ON CACHE BOOL "" FORCE) +foreach(backend METAL CUDA HIP VULKAN SYCL OPENCL OPENVINO RPC MUSA WEBGPU ZDNN + VIRTGPU VIRTGPU_BACKEND ET HEXAGON ZENDNN BLAS ACCELERATE) + set(GGML_${backend} OFF CACHE BOOL "" FORCE) +endforeach() +foreach(feature BACKEND_DL OPENMP BUILD_TESTS BUILD_EXAMPLES) + set(GGML_${feature} OFF CACHE BOOL "" FORCE) +endforeach() +# Avoid host-specific x86 ISA requirements in downstream executables. +foreach(isa SSE42 AVX AVX2 AVX_VNNI BMI2 FMA F16C AVX512 AVX512_VBMI + AVX512_VNNI AVX512_BF16 AMX_TILE AMX_INT8 AMX_BF16) + set(GGML_${isa} OFF CACHE BOOL "" FORCE) +endforeach() +set(CLAP_USE_SYSTEM_ICU OFF CACHE BOOL "" FORCE) +add_subdirectory("${CLAP_ROOT}/ggml" ggml EXCLUDE_FROM_ALL) +add_subdirectory("${CLAP_ROOT}/thirdparty/json" json EXCLUDE_FROM_ALL) +include("${CLAP_ROOT}/cmake/icu.cmake") +add_subdirectory("${CLAP_ROOT}/src" core EXCLUDE_FROM_ALL) + +add_library(clap-rust-native STATIC bridge.cpp) +target_link_libraries(clap-rust-native PRIVATE clap) +target_compile_features(clap-rust-native PRIVATE cxx_std_17) +install(TARGETS clap-rust-native clap ggml ggml-base ggml-cpu clap-icu-i18n clap-icu-uc clap-icu-data + ARCHIVE DESTINATION lib COMPONENT rust) diff --git a/rust/native/bridge.cpp b/rust/native/bridge.cpp new file mode 100644 index 0000000..5314b05 --- /dev/null +++ b/rust/native/bridge.cpp @@ -0,0 +1,87 @@ +// Private ABI for the Rust crate, not a public clap.cpp C API. +#include "clap.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +struct Result { + std::unique_ptr model; + std::vector floats; + std::vector ints; + int rows = 0, cols = 0, rate = 0; + char error[1024] = {}; +}; +static_assert(sizeof(int) == sizeof(int32_t), "Rust tokens require 32-bit int"); +std::vector pcm(const float *p, size_t n, int rate) { + if (rate <= 0 || n == 0 || n > (size_t(1) << 28)) + throw std::invalid_argument("PCM must be nonempty, <=1 GiB, with positive sample rate"); + std::vector v(p, p + n); + for (float x : v) if (!std::isfinite(x)) throw std::invalid_argument("PCM must be finite"); + return v; +} +} +extern "C" { +// All arguments are borrowed for this call. Returned Result owns all outputs; +// cr_free must release it. On allocation failure returns null. Errors use an +// inline buffer, so translating bad_alloc cannot itself allocate or throw. +// Ops: 0=open, 1=tokenize, 2=text, 3=tokens, 4=audio, 5=frames, +// 6=clamp3_features, 7=metadata, 8=decode, 9=resample, 10=mel. +Result *cr_call(int op, clap::Model *model, const char *s, const float *f, + const int *ids, size_t n, int rate, size_t extra) noexcept { + auto result = std::unique_ptr(new (std::nothrow) Result); + if (!result) return nullptr; + try { + switch (op) { + case 0: + if (rate < 0) throw std::invalid_argument("threads must be nonnegative"); + result->model = std::make_unique(s, rate); break; + case 1: + if (!extra) throw std::invalid_argument("max_length must be positive"); + result->ints = model->tokenize(s, extra); break; + case 2: result->floats = model->text(s); break; + case 3: + if (!n) throw std::invalid_argument("tokens must be nonempty"); + result->floats = model->text(std::vector(ids, ids + n)); break; + case 4: result->floats = model->audio(pcm(f, n, rate), rate, extra); break; + case 5: case 6: { + auto input = pcm(f, n, rate); + auto frames = op == 5 ? model->audio_frames(input, rate) : model->clamp3_features(input, rate); + result->rows = frames.frames; result->cols = frames.dimension; + result->floats = std::move(frames.values); break; + } + case 7: result->rate = model->audio_sample_rate(); result->cols = model->projection_dimension(); break; + case 8: { + auto a = clap::decode_audio(s); + result->floats = std::move(a.samples); result->rate = a.sample_rate; break; + } + case 9: { + if (extra > size_t(INT32_MAX)) throw std::invalid_argument("target rate out of range"); + auto a = clap::resample_audio({pcm(f, n, rate), rate}, int(extra)); + result->floats = std::move(a.samples); break; + } + case 10: result->floats = clap::log_mel(pcm(f, n, rate), rate, extra); break; + default: throw std::invalid_argument("unknown operation"); + } + } catch (const std::exception &e) { + std::snprintf(result->error, sizeof(result->error), "%s", e.what()); + } catch (...) { + std::snprintf(result->error, sizeof(result->error), "%s", "unknown native exception"); + } + return result.release(); +} +void cr_free(Result *p) noexcept { delete p; } +void cr_model_free(clap::Model *p) noexcept { delete p; } +clap::Model *cr_model_take(Result *p) noexcept { return p->model.release(); } +const char *cr_error(const Result *p) noexcept { return p->error; } +const float *cr_floats(const Result *p) noexcept { return p->floats.data(); } +const int *cr_ints(const Result *p) noexcept { return p->ints.data(); } +size_t cr_len(const Result *p, int ints) noexcept { return ints ? p->ints.size() : p->floats.size(); } +int cr_meta(const Result *p, int field) noexcept { return field == 0 ? p->rows : field == 1 ? p->cols : p->rate; } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..27ea192 --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,262 @@ +//! Safe, self-contained CPU bindings to the clap.cpp C++ API. +//! +//! Build from a recursive Git checkout; see `rust/README.md`. Native results are +//! copied into Rust-owned vectors. A model is deliberately neither Send nor Sync. +use std::{ + ffi::{c_char, c_void, CStr, CString}, + fmt, + marker::PhantomData, + path::Path, + ptr::NonNull, + rc::Rc, +}; + +/// An owned diagnostic from argument validation or native inference. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Error(String); +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} +impl std::error::Error for Error {} +/// Fallible binding operation. +pub type Result = std::result::Result; + +mod ffi { + use super::*; + unsafe extern "C" { + pub fn cr_call( + op: i32, + model: *mut c_void, + s: *const c_char, + f: *const f32, + ids: *const i32, + n: usize, + rate: i32, + extra: usize, + ) -> *mut c_void; + pub fn cr_free(p: *mut c_void); + pub fn cr_model_free(p: *mut c_void); + pub fn cr_model_take(p: *mut c_void) -> *mut c_void; + pub fn cr_error(p: *const c_void) -> *const c_char; + pub fn cr_floats(p: *const c_void) -> *const f32; + pub fn cr_ints(p: *const c_void) -> *const i32; + pub fn cr_len(p: *const c_void, ints: i32) -> usize; + pub fn cr_meta(p: *const c_void, field: i32) -> i32; + } +} +struct Native(NonNull); +impl Drop for Native { + fn drop(&mut self) { + unsafe { ffi::cr_free(self.0.as_ptr()) } + } +} +impl Native { + fn ptr(&self) -> *mut c_void { + self.0.as_ptr() + } + fn floats(&self) -> Vec { + // C++ owns a contiguous vector until this Native is dropped. Never form + // a Rust slice from vector::data() when empty (it may be null). + unsafe { + let n = ffi::cr_len(self.ptr(), 0); + if n == 0 { + Vec::new() + } else { + std::slice::from_raw_parts(ffi::cr_floats(self.ptr()), n).to_vec() + } + } + } + fn meta(&self, field: i32) -> i32 { + unsafe { ffi::cr_meta(self.ptr(), field) } + } + fn frames(&self) -> Result { + let frames = + usize::try_from(self.meta(0)).map_err(|_| Error("negative frame count".into()))?; + let dimension = + usize::try_from(self.meta(1)).map_err(|_| Error("negative frame dimension".into()))?; + let values = self.floats(); + if frames.checked_mul(dimension) != Some(values.len()) { + return Err(Error("invalid native frame shape".into())); + } + Ok(AudioFrames { + values, + frames, + dimension, + }) + } +} +fn string(s: &str) -> Result { + CString::new(s).map_err(|_| Error("interior NUL is not allowed".into())) +} +fn path(p: &Path) -> Result { + string( + p.to_str() + .ok_or_else(|| Error("path must be valid UTF-8".into()))?, + ) +} +// All pointer/length pairs are constructed from live borrowed slices here, and +// the synchronous bridge copies them before returning. No raw pointers escape. +#[allow(clippy::too_many_arguments)] +fn call( + op: i32, + model: *mut c_void, + s: Option<&CStr>, + pcm: &[f32], + ids: &[i32], + rate: i32, + extra: usize, +) -> Result { + let n = if op == 3 { ids.len() } else { pcm.len() }; + let p = unsafe { + ffi::cr_call( + op, + model, + s.map_or(std::ptr::null(), CStr::as_ptr), + pcm.as_ptr(), + ids.as_ptr(), + n, + rate, + extra, + ) + }; + let result = + Native(NonNull::new(p).ok_or_else(|| Error("native result allocation failed".into()))?); + let error = unsafe { CStr::from_ptr(ffi::cr_error(result.ptr())) }; + if !error.is_empty() { + return Err(Error(error.to_string_lossy().into_owned())); + } + Ok(result) +} + +/// Mono floating-point PCM and its source sample rate. +#[derive(Debug, Clone, PartialEq)] +pub struct Audio { + pub samples: Vec, + pub sample_rate: i32, +} +/// Time-major frames; values has exactly `frames * dimension` elements. +#[derive(Debug, Clone, PartialEq)] +pub struct AudioFrames { + pub values: Vec, + pub frames: usize, + pub dimension: usize, +} + +/// Owned native model. Drop frees weights and all native resources. +/// +/// Inference requires exclusive access and models are deliberately !Send/!Sync: +/// the native backend does not guarantee same-instance thread safety. +/// ```compile_fail +/// fn send() {} +/// send::(); +/// ``` +/// ```compile_fail +/// fn sync() {} +/// sync::(); +/// ``` +pub struct Model { + ptr: NonNull, + _not_send_sync: PhantomData>, +} +impl Drop for Model { + fn drop(&mut self) { + unsafe { ffi::cr_model_free(self.ptr.as_ptr()) } + } +} +impl Model { + /// Load a UTF-8 GGUF path. Zero threads uses the native default. + pub fn new(p: impl AsRef, threads: i32) -> Result { + let s = path(p.as_ref())?; + let native = call(0, std::ptr::null_mut(), Some(&s), &[], &[], threads, 0)?; + let ptr = NonNull::new(unsafe { ffi::cr_model_take(native.ptr()) }) + .ok_or_else(|| Error("native model missing".into()))?; + Ok(Self { + ptr, + _not_send_sync: PhantomData, + }) + } + /// Tokenize UTF-8 up to max_length using the model tokenizer, without padding. + pub fn tokenize(&self, text: &str, max_length: usize) -> Result> { + let s = string(text)?; + let native = call(1, self.ptr.as_ptr(), Some(&s), &[], &[], 0, max_length)?; + unsafe { + let n = ffi::cr_len(native.ptr(), 1); + Ok(if n == 0 { + Vec::new() + } else { + std::slice::from_raw_parts(ffi::cr_ints(native.ptr()), n).to_vec() + }) + } + } + /// Embed one string with the model's native string/context policy. + pub fn text(&mut self, text: &str) -> Result> { + let s = string(text)?; + Ok(call(2, self.ptr.as_ptr(), Some(&s), &[], &[], 0, 0)?.floats()) + } + /// Embed caller-supplied token IDs. Invalid IDs/context lengths are errors. + pub fn text_tokens(&mut self, tokens: &[i32]) -> Result> { + Ok(call(3, self.ptr.as_ptr(), None, &[], tokens, 0, 0)?.floats()) + } + /// Embed finite mono PCM; the native model resamples automatically. + /// Crop offsets count samples in the model's preprocessed input domain. + pub fn audio(&mut self, pcm: &[f32], sample_rate: i32, crop_offset: usize) -> Result> { + Ok(call( + 4, + self.ptr.as_ptr(), + None, + pcm, + &[], + sample_rate, + crop_offset, + )? + .floats()) + } + /// Unpooled final-layer frame sequence (unsupported models return errors). + pub fn audio_frames(&mut self, pcm: &[f32], sample_rate: i32) -> Result { + call(5, self.ptr.as_ptr(), None, pcm, &[], sample_rate, 0)?.frames() + } + /// MERT-v1 all-layer, five-second-window CLaMP3 frontend features. + pub fn clamp3_features(&mut self, pcm: &[f32], sample_rate: i32) -> Result { + call(6, self.ptr.as_ptr(), None, pcm, &[], sample_rate, 0)?.frames() + } + /// Native waveform input rate. + pub fn audio_sample_rate(&self) -> Result { + Ok(call(7, self.ptr.as_ptr(), None, &[], &[], 0, 0)?.meta(2)) + } + /// Width of the model's embedding output. + pub fn projection_dimension(&self) -> Result { + Ok(call(7, self.ptr.as_ptr(), None, &[], &[], 0, 0)?.meta(1)) + } +} +/// Decode WAV/MP3/FLAC by contents, averaging channels; preserves source rate. +pub fn decode_audio(p: impl AsRef) -> Result