Skip to main content

cactus_sys/
needle.rs

1//! Raw declarations for the Needle 3 engine C API (`include/needle.h`).
2//!
3//! ## Overview
4//!
5//! Nine functions, no handles, no opaque types. The engine owns one process-global model per
6//! kind: a text model ([`NEEDLE_TEXT`], Needle 3) and a speech model ([`NEEDLE_SPEECH`],
7//! Whistle). [`needle_load`] reads whichever kind an archive holds and keeps the other, so both
8//! can live in one process; [`needle_models`] reports which are loaded.
9//!
10//! The text model is configured with [`needle_init`], answers with [`needle_complete`] and is
11//! rewound by [`needle_reset`]. The speech model transcribes with [`needle_transcribe`], and also
12//! runs inside [`needle_complete`] when it is handed a clip instead of a string, configured by
13//! [`needle_set_audio`]. [`needle_embed`] embeds either kind of input.
14//!
15//! The declarations are written by hand rather than generated. Nine stable entry points do not
16//! justify a `bindgen` build dependency, and hand-written signatures let every function carry the
17//! `# Safety` contract that was measured against the shipped archive.
18//!
19//! Every function that can fail returns a negative value when it does, and [`needle_last_error`]
20//! then names the reason. That string is one process-global slot shared by both kinds, owned by
21//! the runtime and overwritten by the next call: copy it before calling anything else.
22//!
23//! Audio is 16 kHz mono `f32` PCM in `[-1, 1]`, at most 30 seconds (480 000 samples) per call.
24//!
25//! ## Usage
26//!
27//! ```no_run
28//! use core::ffi::{c_char, c_int, c_ulonglong};
29//!
30//! use cactus_sys::{NEEDLE_TEXT, needle_complete, needle_init, needle_load, needle_models};
31//!
32//! let weights: &[u8] = b"...contents of needle3.cact...";
33//! let mut out = [0u8; 64 * 1024];
34//!
35//! // SAFETY: every call below happens on one thread, in order, and the engine copies the
36//! // archive during `needle_load`, so `weights` needs no particular lifetime afterwards.
37//! unsafe {
38//!     assert_eq!(needle_load(weights.as_ptr(), weights.len() as c_ulonglong), 0);
39//!     assert_ne!(needle_models() & NEEDLE_TEXT, 0);
40//!     assert!(needle_init(c"You are terse.".as_ptr(), c"[]".as_ptr(), core::ptr::null()) > 0);
41//!
42//!     let tokens = needle_complete(
43//!         c"hello".as_ptr(),
44//!         core::ptr::null(),
45//!         0,
46//!         64,
47//!         out.as_mut_ptr().cast::<c_char>(),
48//!         out.len() as c_int,
49//!     );
50//!     assert!(tokens >= 0);
51//! }
52//! ```
53
54use core::ffi::{c_char, c_float, c_int, c_uchar, c_ulonglong};
55
56/// Commit of the `Cactus-Compute/needle3` Hugging Face repository this crate is pinned to.
57///
58/// It is the `commit` field of `cactus-sys/prebuilt.toml`, the same revision the
59/// `download-binaries` feature fetches archives from, threaded through by the build script so
60/// that the pin has exactly one source of truth.
61///
62/// # Examples
63///
64/// ```
65/// assert_eq!(cactus_sys::NEEDLE_ENGINE_COMMIT.len(), 40);
66/// ```
67pub const NEEDLE_ENGINE_COMMIT: &str = env!("NEEDLE_ENGINE_COMMIT");
68
69/// Bit set in [`needle_models`] when a text model (Needle 3) is loaded.
70pub const NEEDLE_TEXT: c_int = 1;
71
72/// Bit set in [`needle_models`] when a speech model (Whistle) is loaded.
73pub const NEEDLE_SPEECH: c_int = 2;
74
75unsafe extern "C" {
76    /// Configures the conversation prefix: system prompt, tool declarations and tool index.
77    ///
78    /// Returns the number of tokens in the resulting prefix, always greater than zero, or a
79    /// negative value when it fails, most commonly because no text model has been loaded yet;
80    /// [`needle_last_error`] then names the reason. Calling it again replaces the previous
81    /// configuration.
82    ///
83    /// Tool declarations are flat objects (`{name, description, parameters, triggers?}`), not the
84    /// OpenAI-style nested form. **The engine does not validate `tools_json`**: malformed JSON
85    /// still returns a success value, and the damage only shows up later as a model that ignores
86    /// the tools. Validate the document before handing it over.
87    ///
88    /// # Safety
89    ///
90    /// - `system_prompt`, `tools_json` and `tool_index_path` must each be either null or a
91    ///   pointer to a NUL-terminated C string that stays valid for the duration of the call.
92    ///   `system_prompt` and `tool_index_path` are documented as optional; passing null for
93    ///   `tools_json` declares no tools.
94    /// - The engine is one process-global, non-thread-safe runtime for both kinds of model. No
95    ///   call in this module may run concurrently with any other; the caller must serialise
96    ///   all of them.
97    /// - A text model must have been loaded with [`needle_load`] first, otherwise this call
98    ///   fails. Loading a text archive again, even the same one, discards the configuration:
99    ///   call this again afterwards.
100    pub fn needle_init(
101        system_prompt: *const c_char,
102        tools_json: *const c_char,
103        tool_index_path: *const c_char,
104    ) -> c_int;
105
106    /// Runs one completion on text or on a speech clip and writes a JSON envelope into `out`.
107    ///
108    /// Exactly one of `input` and `pcm` is non-null. Given `pcm`, the engine transcribes the
109    /// clip with the speech model, as configured by [`needle_set_audio`], answers the
110    /// transcript with the text model, and merges the speech fields into the same JSON object
111    /// under an `audio_` prefix (`audio_text`, `audio_language`, ...).
112    ///
113    /// Returns a non-negative **token count** on success (not the number of bytes written) and
114    /// a negative value on failure, with the reason in [`needle_last_error`]. On failure `out`
115    /// also holds a NUL-terminated error envelope carrying the same text, such as
116    /// `{"type":"error","error":"needle_init not called"}`. A failed audio completion may still
117    /// carry the `audio_*` fields of a transcription that ran before the failure.
118    ///
119    /// `out` is always NUL-terminated, and the envelope is **silently truncated** to
120    /// `out_capacity - 1` bytes when it does not fit: there is no error and no length hint, so a
121    /// short buffer surfaces as JSON that fails to parse. An `out_capacity` of `0` is tolerated
122    /// and writes nothing. Size the buffer generously; 64 KiB is comfortable for tool calls, and
123    /// an audio completion with word timestamps needs room for every word on top.
124    ///
125    /// # Safety
126    ///
127    /// - Exactly one of `input` and `pcm` must be non-null.
128    /// - `input` must be null or a pointer to a NUL-terminated C string valid for the call.
129    /// - `pcm` must be null, or valid for reads of `samples` `f32` values for the duration of
130    ///   the call, holding 16 kHz mono audio in `[-1, 1]`. `samples` must not be negative and
131    ///   must not exceed 480 000 (30 seconds); with `pcm` null it is ignored and `0` is
132    ///   conventional.
133    /// - `out` must be valid for writes of `out_capacity` bytes, and `out_capacity` must not be
134    ///   negative or larger than the allocation behind `out`.
135    /// - The engine is one process-global, non-thread-safe runtime for both kinds of model; the
136    ///   caller must serialise every call in this module.
137    /// - A text model must be loaded and [`needle_init`] must have succeeded first, and a
138    ///   speech model must also be loaded when `pcm` is non-null; otherwise this fails.
139    pub fn needle_complete(
140        input: *const c_char,
141        pcm: *const c_float,
142        samples: c_int,
143        max_new_tokens: c_int,
144        out: *mut c_char,
145        out_capacity: c_int,
146    ) -> c_int;
147
148    /// Configures the transcription that [`needle_complete`] runs for itself on a clip.
149    ///
150    /// `language`, `keywords` and `word_timestamps` mean what they mean in
151    /// [`needle_transcribe`]. The setting is process-global and stays in force until the next
152    /// call; the defaults are to detect the language, use no keywords and report no word times.
153    /// The engine **copies** both strings, so they may be freed as soon as this returns. It
154    /// cannot fail and returns nothing. The setting survives [`needle_reset`] and
155    /// [`needle_init`], and [`needle_transcribe`] ignores it: that function takes its own.
156    ///
157    /// # Safety
158    ///
159    /// - `language` and `keywords` must each be either null or a pointer to a NUL-terminated C
160    ///   string valid for the duration of the call.
161    /// - The engine is one process-global, non-thread-safe runtime for both kinds of model; the
162    ///   caller must serialise every call in this module.
163    pub fn needle_set_audio(
164        language: *const c_char,
165        keywords: *const c_char,
166        word_timestamps: c_int,
167    );
168
169    /// Transcribes one speech clip and writes a JSON envelope into `out`.
170    ///
171    /// Returns the number of tokens generated on success and a negative value on failure, with
172    /// the reason in [`needle_last_error`]. The envelope has the shape
173    /// `{"text":"...","language":"en","ttft_ms":0.0,"decode_tps":0.0}`; a non-zero
174    /// `word_timestamps` adds `"words":[{"word":"...","start":0.00,"end":0.00,"probability":0.000}]`
175    /// with times in seconds. Silence and steady noise give an empty `text` and `language`.
176    ///
177    /// `language` is one of `"en"`, `"de"`, `"fr"`, `"es"`, `"it"`, `"nl"` or `"pl"`, or null to
178    /// detect it. Codes are case-sensitive, the empty string also means detect, and any other
179    /// code fails with `unknown language <code>`. Forcing a language steers the decoder and
180    /// labels the result; it does not translate. `keywords` is null or newline-separated words
181    /// and phrases to bias the decoder towards. An empty clip (`samples` 0, `pcm` non-null) is
182    /// not an error: it gives the silence envelope.
183    ///
184    /// `out` follows the rules of [`needle_complete`]: always NUL-terminated and **silently
185    /// truncated** to `out_capacity - 1` bytes when it does not fit, with the token count still
186    /// returned. Unlike [`needle_complete`], an `out_capacity` of `0` or a null `out` fails with
187    /// "no output buffer". A failed call writes no envelope; the reason is only in
188    /// [`needle_last_error`].
189    ///
190    /// # Safety
191    ///
192    /// - `pcm` must be valid for reads of `samples` `f32` values for the duration of the call,
193    ///   holding 16 kHz mono audio in `[-1, 1]`. `samples` must not be negative and must not
194    ///   exceed 480 000 (30 seconds).
195    /// - `language` and `keywords` must each be either null or a pointer to a NUL-terminated C
196    ///   string valid for the duration of the call.
197    /// - `out` must be null or valid for writes of `out_capacity` bytes, and `out_capacity`
198    ///   must not be negative or larger than the allocation behind `out`.
199    /// - The engine is one process-global, non-thread-safe runtime for both kinds of model; the
200    ///   caller must serialise every call in this module.
201    /// - A speech model must have been loaded with [`needle_load`] first, otherwise this call
202    ///   fails.
203    pub fn needle_transcribe(
204        pcm: *const c_float,
205        samples: c_int,
206        language: *const c_char,
207        keywords: *const c_char,
208        word_timestamps: c_int,
209        out: *mut c_char,
210        out_capacity: c_int,
211    ) -> c_int;
212
213    /// Embeds text or a speech clip, or reports how many floats that takes.
214    ///
215    /// Exactly one of `input` and `pcm` is non-null. Text yields one vector of the model's
216    /// embedding dimension (3072 for Needle 3, and asking with an empty string returns it
217    /// without computing). Speech yields one row of the speech model's width (512 for the
218    /// pinned Whistle, not the text dimension) per 80 ms frame, flattened, with
219    /// `frames = max(1, (samples + 1120) / 1280)`: an empty clip still gives one row. With
220    /// `out` null, returns that float count without computing anything. With a buffer, returns
221    /// the same count after filling it, or a negative value when `out_capacity` is too small or
222    /// the call fails, with the reason in
223    /// [`needle_last_error`]. Query the count first and size the buffer from the answer.
224    ///
225    /// # Safety
226    ///
227    /// - Exactly one of `input` and `pcm` must be non-null.
228    /// - `input` must be null or a pointer to a NUL-terminated C string valid for the call.
229    /// - `pcm` must be null, or valid for reads of `samples` `f32` values for the duration of
230    ///   the call, holding 16 kHz mono audio in `[-1, 1]`. `samples` must not be negative and
231    ///   must not exceed 480 000 (30 seconds); with `pcm` null it is ignored and `0` is
232    ///   conventional.
233    /// - `out` must be null, or valid for writes of `out_capacity` `f32` values; `out_capacity`
234    ///   must not be negative or exceed the allocation behind `out`.
235    /// - The engine is one process-global, non-thread-safe runtime for both kinds of model; the
236    ///   caller must serialise every call in this module.
237    /// - A model of the input's kind must have been loaded with [`needle_load`] first.
238    pub fn needle_embed(
239        input: *const c_char,
240        pcm: *const c_float,
241        samples: c_int,
242        out: *mut c_float,
243        out_capacity: c_int,
244    ) -> c_int;
245
246    /// Rewinds the conversation to the prefix that [`needle_init`] built.
247    ///
248    /// Tool declarations and the system prompt survive; only the turns taken since are dropped.
249    /// It cannot fail and it does not unload weights. Nothing in this API does.
250    ///
251    /// # Safety
252    ///
253    /// - The engine is one process-global, non-thread-safe runtime for both kinds of model; the
254    ///   caller must serialise every call in this module. In particular this must not run while
255    ///   any other call, of either kind, is in flight.
256    pub fn needle_reset();
257
258    /// Loads a `.cact` weight archive from memory.
259    ///
260    /// The engine reads whichever kind of model the archive holds, text or speech, and keeps a
261    /// model of the other kind already loaded, so a process with both takes one call per
262    /// archive. Both kinds carry the same magic tag: check [`needle_models`] afterwards to learn
263    /// which one was loaded.
264    ///
265    /// Returns `0` on success and a negative value on failure, including for a null pointer or
266    /// bytes that are not a valid archive, with the reason in [`needle_last_error`]. The engine
267    /// **copies** what it needs during the call, so the buffer may be freed as soon as it
268    /// returns.
269    ///
270    /// Weights cannot be unloaded. Loading a text archive again, even the one already loaded,
271    /// discards the [`needle_init`] configuration; the speech model has none to lose.
272    ///
273    /// # Safety
274    ///
275    /// - `cact` must be null, or valid for reads of `n` bytes for the duration of the call.
276    /// - `n` must be the length of that buffer in bytes.
277    /// - The engine is one process-global, non-thread-safe runtime for both kinds of model; the
278    ///   caller must serialise every call in this module.
279    pub fn needle_load(cact: *const c_uchar, n: c_ulonglong) -> c_int;
280
281    /// Reports which kinds of model this process has loaded.
282    ///
283    /// Returns a bitmask of [`NEEDLE_TEXT`] and [`NEEDLE_SPEECH`]: `0` before any successful
284    /// [`needle_load`], and never fewer bits afterwards, since nothing unloads weights. It cannot
285    /// fail.
286    ///
287    /// # Safety
288    ///
289    /// - The engine is one process-global, non-thread-safe runtime for both kinds of model; the
290    ///   caller must serialise every call in this module.
291    pub fn needle_models() -> c_int;
292
293    /// Returns the reason the last failing call failed.
294    ///
295    /// The result is a NUL-terminated C string **owned by the runtime**. It is one
296    /// process-global slot shared by both kinds of model, and it is valid only until the next
297    /// call into this module, which may overwrite or free it: copy it out before calling
298    /// anything else, `needle_last_error` included.
299    ///
300    /// # Safety
301    ///
302    /// - The returned pointer may be null; check before dereferencing it.
303    /// - It must not be written through, freed, or read after the next call into this module.
304    /// - The engine is one process-global, non-thread-safe runtime for both kinds of model; the
305    ///   caller must serialise every call in this module, and must copy the string before
306    ///   releasing whatever serialises them.
307    pub fn needle_last_error() -> *const c_char;
308}