react-native-nobodywho 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nobodywho",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "Run LLMs locally with offline inference — React Native bindings",
5
5
  "main": "src/wrapper.ts",
6
6
  "types": "src/wrapper.ts",
package/src/chat.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  RustChat,
3
3
  SamplerConfig,
4
+ type ChatStats,
4
5
  } from "../generated/ts/nobodywho";
5
6
  import { Model } from "./model";
6
7
  import { type Message, fromInternal, toInternal } from "./message";
@@ -85,7 +86,10 @@ export class Chat {
85
86
  if (typeof message === "string") {
86
87
  return new TokenStream(this._inner.ask(message));
87
88
  }
88
- return new TokenStream(this._inner.askWithPrompt(message._parts));
89
+ if (message._jsonString !== null) {
90
+ return new TokenStream(this._inner.askWithJsonPrompt(message._jsonString!));
91
+ }
92
+ return new TokenStream(this._inner.askWithPrompt(message._parts!));
89
93
  }
90
94
 
91
95
  /** Stop the current generation. */
@@ -125,6 +129,15 @@ export class Chat {
125
129
  return this._inner.getSystemPrompt();
126
130
  }
127
131
 
132
+ /** Tokenize a text message or multimodal prompt and return the token IDs.
133
+ * Each element is a token ID (number) for text, or null for image/audio embedding slots. */
134
+ async tokenize(message: string | Prompt): Promise<(number | null)[]> {
135
+ if (typeof message === "string") {
136
+ return this._inner.tokenize(message);
137
+ }
138
+ return this._inner.tokenizeWithPrompt(message._parts);
139
+ }
140
+
128
141
  /** Set the system prompt. */
129
142
  async setSystemPrompt(systemPrompt: string | undefined): Promise<void> {
130
143
  return this._inner.setSystemPrompt(systemPrompt);
@@ -155,6 +168,11 @@ export class Chat {
155
168
  return this._inner.getSamplerConfigJson();
156
169
  }
157
170
 
171
+ /** Get context usage statistics. */
172
+ async getStats(): Promise<ChatStats> {
173
+ return this._inner.getStats();
174
+ }
175
+
158
176
  /**
159
177
  * Immediately free the underlying Rust resources (model context, KV cache, etc.).
160
178
  * After calling this, the Chat instance is no longer usable.
package/src/message.ts CHANGED
@@ -74,7 +74,7 @@ export function toInternal(msg: Message): InternalMessage {
74
74
  } else if (msg.role === "assistant") {
75
75
  return new InternalMessage.Assistant({
76
76
  content: msg.content,
77
- toolCalls: null,
77
+ toolCalls: undefined,
78
78
  });
79
79
  } else if (msg.role === "system") {
80
80
  return new InternalMessage.System({
package/src/model.ts CHANGED
@@ -60,6 +60,14 @@ export class Model {
60
60
  return new Model(inner);
61
61
  }
62
62
 
63
+ /**
64
+ * The maximum context size this model was trained with.
65
+ * Use this to avoid setting `contextSize` above what the model supports.
66
+ */
67
+ get maxCtx(): number {
68
+ return Number((this._inner as any).maxCtx());
69
+ }
70
+
63
71
  /**
64
72
  * Immediately free the underlying Rust resources (model weights, GPU memory).
65
73
  * After calling this, the Model instance is no longer usable.
package/src/prompt.ts CHANGED
@@ -45,14 +45,21 @@ type Part = TextPart | ImagePart | AudioPart;
45
45
  * ]);
46
46
  *
47
47
  * const stream = chat.ask(prompt);
48
+ *
49
+ * // JSON-structured prompt
50
+ * const jsonPrompt = Prompt.fromJson({ role: "user", content: "Hello" });
51
+ * const stream = chat.ask(jsonPrompt);
48
52
  * ```
49
53
  */
50
54
  export class Prompt {
51
55
  /** @internal */
52
- readonly _parts: PromptPart[];
56
+ readonly _parts: PromptPart[] | null;
57
+ /** @internal */
58
+ readonly _jsonString: string | null;
53
59
 
54
60
  constructor(parts: Part[]) {
55
61
  this._parts = parts.map((p) => p._inner);
62
+ this._jsonString = null;
56
63
  }
57
64
 
58
65
  /** Create a text part. */
@@ -69,4 +76,11 @@ export class Prompt {
69
76
  static Audio(path: string): AudioPart {
70
77
  return new AudioPart(path);
71
78
  }
79
+
80
+ /** Create a prompt from any JSON-serializable value (object, array, string, etc.). */
81
+ static fromJson(data: unknown): Prompt {
82
+ const p: Prompt = Object.create(Prompt.prototype);
83
+ Object.assign(p, { _parts: null, _jsonString: JSON.stringify(data) });
84
+ return p;
85
+ }
72
86
  }
package/src/stt.ts ADDED
@@ -0,0 +1,69 @@
1
+ import type { RustSttInterface } from "../generated/ts/nobodywho";
2
+ import * as nobodywho from "../generated/ts/nobodywho";
3
+ import { TokenStream } from "./streaming";
4
+
5
+ export type SttOptions = {
6
+ source: string;
7
+ language?: string;
8
+ quantization?: string;
9
+ };
10
+
11
+ /**
12
+ * Speech-to-text using a local Whisper ONNX model.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const stt = new STT({
17
+ * source: "hf://onnx-community/whisper-base",
18
+ * language: "en",
19
+ * });
20
+ * for await (const piece of stt.transcribeFile("recording.mp3")) {
21
+ * process.stdout.write(piece);
22
+ * }
23
+ *
24
+ * // From microphone (react-native-audio-pcm-stream gives Int16Array):
25
+ * const stream = stt.transcribePcm(samplesI16, 44100);
26
+ * const text = await stream.completed();
27
+ * ```
28
+ */
29
+ export class STT {
30
+ /** @internal */
31
+ private readonly _inner: RustSttInterface;
32
+
33
+ /**
34
+ * @param opts - See {@link SttOptions}.
35
+ */
36
+ constructor(opts: SttOptions) {
37
+ this._inner = new nobodywho.RustStt(
38
+ opts.source,
39
+ opts.language,
40
+ opts.quantization,
41
+ );
42
+ }
43
+
44
+ /**
45
+ * Transcribe an audio file (WAV / MP3).
46
+ * Returns an `STTStream` to consume tokens as they arrive.
47
+ */
48
+ transcribeFile(path: string): TokenStream {
49
+ return new TokenStream(this._inner.transcribeFile(path));
50
+ }
51
+
52
+ /**
53
+ * Transcribe raw i16 PCM samples (e.g. from a microphone).
54
+ * The backend resamples to 16 kHz internally, so pass whatever rate your
55
+ * mic captures at (typically 44100 or 48000).
56
+ *
57
+ * @param samples - Flat array of signed 16-bit samples (mono).
58
+ * @param sampleRate - Capture rate in Hz.
59
+ */
60
+ transcribePcm(samples: Int16Array | number[], sampleRate: number): TokenStream {
61
+ const arr = samples instanceof Int16Array ? Array.from(samples) : samples;
62
+ return new TokenStream(this._inner.transcribePcm(arr, sampleRate));
63
+ }
64
+
65
+ /** Release native resources. Call when done with this handle. */
66
+ destroy(): void {
67
+ (this._inner as any).uniffiDestroy?.();
68
+ }
69
+ }
package/src/tts.ts ADDED
@@ -0,0 +1,78 @@
1
+ import {
2
+ RustTts,
3
+ loadTts,
4
+ type RustTtsInterface,
5
+ } from "../generated/ts/nobodywho";
6
+
7
+ export type TtsArchitecture = "kokoro" | "supertonic";
8
+ export type TtsDevice = "auto" | "cpu" | "cuda";
9
+
10
+ export type TtsOptions = {
11
+ source: string;
12
+ architecture?: TtsArchitecture;
13
+ voice?: string;
14
+ language?: string;
15
+ speed?: number;
16
+ steps?: number;
17
+ silenceDuration?: number;
18
+ device?: TtsDevice;
19
+ };
20
+
21
+ /** Text-to-speech synthesizer that returns WAV bytes. */
22
+ export class Tts {
23
+ /** @internal */
24
+ private _inner: RustTtsInterface;
25
+
26
+ /** Create a TTS synthesizer synchronously. */
27
+ constructor(opts: TtsOptions) {
28
+ this._inner = new RustTts(
29
+ opts.source,
30
+ opts.architecture,
31
+ opts.voice,
32
+ opts.language,
33
+ opts.speed,
34
+ opts.steps,
35
+ opts.silenceDuration,
36
+ opts.device ?? "auto",
37
+ );
38
+ }
39
+
40
+ private static fromInner(inner: RustTtsInterface): Tts {
41
+ const tts = Object.create(Tts.prototype) as Tts;
42
+ tts._inner = inner;
43
+ return tts;
44
+ }
45
+
46
+ /** Create a TTS synthesizer asynchronously. */
47
+ static async load(opts: TtsOptions): Promise<Tts> {
48
+ const inner = await loadTts(
49
+ opts.source,
50
+ opts.architecture,
51
+ opts.voice,
52
+ opts.language,
53
+ opts.speed,
54
+ opts.steps,
55
+ opts.silenceDuration,
56
+ opts.device ?? "auto",
57
+ );
58
+ return Tts.fromInner(inner);
59
+ }
60
+
61
+ /** Synthesize text and return WAV bytes. */
62
+ async synthesize(text: string): Promise<Uint8Array> {
63
+ return new Uint8Array(await this._inner.synthesizeAsync(text));
64
+ }
65
+
66
+ /** Synthesize text synchronously and return WAV bytes. */
67
+ synthesizeSync(text: string): Uint8Array {
68
+ return new Uint8Array(this._inner.synthesize(text));
69
+ }
70
+
71
+ /**
72
+ * Immediately free the underlying Rust resources.
73
+ * After calling this, the Tts instance is no longer usable.
74
+ */
75
+ destroy(): void {
76
+ (this._inner as any).uniffiDestroy();
77
+ }
78
+ }
package/src/wrapper.ts CHANGED
@@ -2,19 +2,65 @@
2
2
  * NobodyWho — React Native bindings for local LLM inference.
3
3
  *
4
4
  * This is the public entry point. It re-exports the public API:
5
- * - Wrapper classes: Model, Chat, Encoder, CrossEncoder, Tool, TokenStream, Prompt
5
+ * - Wrapper classes: Model, Chat, Encoder, CrossEncoder, Tts, Tool, TokenStream, Prompt
6
6
  * - Direct re-exports: SamplerBuilder, SamplerConfig
7
7
  * - Types: Message, Asset, ToolCall
8
- * - Utilities: SamplerPresets, cosineSimilarity
8
+ * - Utilities: SamplerPresets, cosineSimilarity, downloadModel
9
9
  *
10
10
  * This file is NOT generated — it is safe to edit.
11
11
  */
12
12
 
13
+ // Native crate install + bindings initialize. These run as side effects of
14
+ // evaluating this module, which the React Native runtime is guaranteed to do
15
+ // before any FFI call because wrapper.ts is the package main.
16
+ //
17
+ // The install logic was previously reached only through lazy re-exports from
18
+ // ./index. In release mode Metro's inlineRequires defers lazy re-exports
19
+ // until first use, so consumers whose first interaction was Model.load /
20
+ // downloadModel / Chat never triggered ./index and FFI calls then threw
21
+ // "Cannot read property 'ubrn_…' of undefined". Inlining the install here
22
+ // removes the ./index indirection so this can't recur.
23
+ import installer from "./NativeNobodywho";
24
+ import nobodywhoDefault, {
25
+ downloadModel as _downloadModel,
26
+ } from "../generated/ts/nobodywho";
27
+
28
+ installer.installRustCrate();
29
+ nobodywhoDefault.initialize();
30
+
31
+ /**
32
+ * Download a GGUF model from a remote URL or HuggingFace path and return the local file path.
33
+ *
34
+ * Use this when you need custom HTTP headers, e.g. for gated models that require
35
+ * authentication. For unauthenticated downloads, pass the URL directly to `Model.load`.
36
+ *
37
+ * @param opts.modelPath - Path or URL to a GGUF model file (`hf://owner/repo/file.gguf`, `https://`, or a local path)
38
+ * @param opts.headers - Optional HTTP headers (e.g. `{ Authorization: "Bearer hf_..." }`)
39
+ * @param opts.onDownloadProgress - Optional callback receiving `(downloadedBytes, totalBytes)` during download
40
+ * @returns The local filesystem path where the model was cached
41
+ */
42
+ export async function downloadModel(opts: {
43
+ modelPath: string;
44
+ headers?: Record<string, string>;
45
+ onDownloadProgress?: (downloaded: number, total: number) => void;
46
+ }): Promise<string> {
47
+ const headersMap = opts.headers
48
+ ? new Map(Object.entries(opts.headers))
49
+ : undefined;
50
+ const cb = opts.onDownloadProgress;
51
+ const progressCallback = cb
52
+ ? { onDownloadProgress: (d: bigint, t: bigint) => cb(Number(d), Number(t)) }
53
+ : undefined;
54
+ return _downloadModel(opts.modelPath, headersMap, progressCallback);
55
+ }
56
+
13
57
  // Wrapper classes
14
58
  export { Model } from "./model";
15
59
  export { Chat } from "./chat";
60
+ export { STT } from "./stt";
16
61
  export { Encoder } from "./encoder";
17
62
  export { CrossEncoder } from "./cross_encoder";
63
+ export { Tts } from "./tts";
18
64
  export { Prompt } from "./prompt";
19
65
  export { Tool } from "./tool";
20
66
  export { TokenStream } from "./streaming";
@@ -23,16 +69,24 @@ export { TokenStream } from "./streaming";
23
69
  export type { Message } from "./message";
24
70
 
25
71
  // Re-export types that don't need wrapping.
72
+ //
73
+ // Note: `getCachedModels` returns `size` as `bigint` (Rust `u64`). Wrap with
74
+ // `Number(m.size)` at the call site if a JS number is needed.
26
75
  export {
27
76
  SamplerBuilder,
28
77
  SamplerConfig,
29
78
  cosineSimilarity,
30
- } from "./index";
79
+ getCachedModels,
80
+ } from "../generated/ts/nobodywho";
31
81
 
32
82
  export type {
33
83
  Asset,
34
84
  ToolCall,
35
- } from "./index";
85
+ CachedModel,
86
+ ChatStats,
87
+ } from "../generated/ts/nobodywho";
88
+ export type { TtsArchitecture, TtsDevice, TtsOptions } from "./tts";
89
+ export type { SttOptions } from "./stt";
36
90
 
37
91
  // Ergonomic wrapper additions.
38
92
  export { SamplerPresets } from "./sampler_presets";