react-native-nobodywho 2.3.0 → 2.5.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/generated/cpp/nobodywho.cpp +1031 -5
- package/generated/cpp/nobodywho.hpp +126 -0
- package/generated/ts/nobodywho-ffi.ts +41 -2
- package/generated/ts/nobodywho.ts +962 -13
- package/package.json +1 -1
- package/src/chat.ts +38 -1
- package/src/message.ts +1 -1
- package/src/model.ts +14 -1
- package/src/prompt.ts +15 -1
- package/src/stt.ts +69 -0
- package/src/tts.ts +87 -0
- package/src/wrapper.ts +25 -4
package/package.json
CHANGED
package/src/chat.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
RustChat,
|
|
3
3
|
SamplerConfig,
|
|
4
|
+
MtpConfig,
|
|
5
|
+
type ChatStats,
|
|
4
6
|
} from "../generated/ts/nobodywho";
|
|
5
7
|
import { Model } from "./model";
|
|
6
8
|
import { type Message, fromInternal, toInternal } from "./message";
|
|
@@ -30,6 +32,12 @@ export class Chat {
|
|
|
30
32
|
/** @internal */
|
|
31
33
|
private readonly _inner: RustChat;
|
|
32
34
|
|
|
35
|
+
/**
|
|
36
|
+
* `threadCount` is the number of CPU threads used for inference. Omit it to detect the
|
|
37
|
+
* device's physical core count (performance cores only, on Apple silicon) — hyperthreads
|
|
38
|
+
* and efficiency cores make inference slower, not faster. Lower it to leave CPU headroom
|
|
39
|
+
* for the rest of the app.
|
|
40
|
+
*/
|
|
33
41
|
constructor(opts: {
|
|
34
42
|
model: Model;
|
|
35
43
|
systemPrompt?: string;
|
|
@@ -37,6 +45,8 @@ export class Chat {
|
|
|
37
45
|
templateVariables?: Record<string, boolean>;
|
|
38
46
|
tools?: Tool[];
|
|
39
47
|
sampler?: SamplerConfig;
|
|
48
|
+
mtp?: Partial<MtpConfig>;
|
|
49
|
+
threadCount?: number;
|
|
40
50
|
}) {
|
|
41
51
|
this._inner = new RustChat(
|
|
42
52
|
opts.model._inner,
|
|
@@ -45,6 +55,8 @@ export class Chat {
|
|
|
45
55
|
opts.templateVariables ? new Map(Object.entries(opts.templateVariables)) : undefined,
|
|
46
56
|
opts.tools?.map((t) => t._inner) ?? undefined,
|
|
47
57
|
opts.sampler ?? undefined,
|
|
58
|
+
opts.mtp !== undefined ? MtpConfig.create(opts.mtp) : undefined,
|
|
59
|
+
opts.threadCount ?? undefined,
|
|
48
60
|
);
|
|
49
61
|
}
|
|
50
62
|
|
|
@@ -64,17 +76,21 @@ export class Chat {
|
|
|
64
76
|
modelPath: string;
|
|
65
77
|
useGpu?: boolean;
|
|
66
78
|
projectionModelPath?: string;
|
|
79
|
+
draftModelPath?: string;
|
|
67
80
|
systemPrompt?: string;
|
|
68
81
|
contextSize?: number;
|
|
69
82
|
templateVariables?: Record<string, boolean>;
|
|
70
83
|
tools?: Tool[];
|
|
71
84
|
sampler?: SamplerConfig;
|
|
85
|
+
mtp?: Partial<MtpConfig>;
|
|
86
|
+
threadCount?: number;
|
|
72
87
|
onDownloadProgress?: (downloaded: number, total: number) => void;
|
|
73
88
|
}): Promise<Chat> {
|
|
74
89
|
const model = await Model.load({
|
|
75
90
|
modelPath: opts.modelPath,
|
|
76
91
|
useGpu: opts.useGpu,
|
|
77
92
|
projectionModelPath: opts.projectionModelPath,
|
|
93
|
+
draftModelPath: opts.draftModelPath,
|
|
78
94
|
onDownloadProgress: opts.onDownloadProgress,
|
|
79
95
|
});
|
|
80
96
|
return new Chat({ model, ...opts });
|
|
@@ -85,7 +101,10 @@ export class Chat {
|
|
|
85
101
|
if (typeof message === "string") {
|
|
86
102
|
return new TokenStream(this._inner.ask(message));
|
|
87
103
|
}
|
|
88
|
-
|
|
104
|
+
if (message._jsonString !== null) {
|
|
105
|
+
return new TokenStream(this._inner.askWithJsonPrompt(message._jsonString!));
|
|
106
|
+
}
|
|
107
|
+
return new TokenStream(this._inner.askWithPrompt(message._parts!));
|
|
89
108
|
}
|
|
90
109
|
|
|
91
110
|
/** Stop the current generation. */
|
|
@@ -125,6 +144,15 @@ export class Chat {
|
|
|
125
144
|
return this._inner.getSystemPrompt();
|
|
126
145
|
}
|
|
127
146
|
|
|
147
|
+
/** Tokenize a text message or multimodal prompt and return the token IDs.
|
|
148
|
+
* Each element is a token ID (number) for text, or null for image/audio embedding slots. */
|
|
149
|
+
async tokenize(message: string | Prompt): Promise<(number | null)[]> {
|
|
150
|
+
if (typeof message === "string") {
|
|
151
|
+
return this._inner.tokenize(message);
|
|
152
|
+
}
|
|
153
|
+
return this._inner.tokenizeWithPrompt(message._parts);
|
|
154
|
+
}
|
|
155
|
+
|
|
128
156
|
/** Set the system prompt. */
|
|
129
157
|
async setSystemPrompt(systemPrompt: string | undefined): Promise<void> {
|
|
130
158
|
return this._inner.setSystemPrompt(systemPrompt);
|
|
@@ -155,6 +183,15 @@ export class Chat {
|
|
|
155
183
|
return this._inner.getSamplerConfigJson();
|
|
156
184
|
}
|
|
157
185
|
|
|
186
|
+
/** Get context usage statistics. */
|
|
187
|
+
async getStats(): Promise<ChatStats> {
|
|
188
|
+
return this._inner.getStats();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async mtpAcceptanceRate(): Promise<number | undefined> {
|
|
192
|
+
return this._inner.mtpAcceptanceRate();
|
|
193
|
+
}
|
|
194
|
+
|
|
158
195
|
/**
|
|
159
196
|
* Immediately free the underlying Rust resources (model context, KV cache, etc.).
|
|
160
197
|
* 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:
|
|
77
|
+
toolCalls: undefined,
|
|
78
78
|
});
|
|
79
79
|
} else if (msg.role === "system") {
|
|
80
80
|
return new InternalMessage.System({
|
package/src/model.ts
CHANGED
|
@@ -36,15 +36,19 @@ export class Model {
|
|
|
36
36
|
/**
|
|
37
37
|
* Load a GGUF model from a local path or remote URL.
|
|
38
38
|
*
|
|
39
|
-
* @param opts.modelPath -
|
|
39
|
+
* @param opts.modelPath - Local path, `hf://` or `https://` URL, or `auto` for memory-based model selection
|
|
40
40
|
* @param opts.useGpu - Use GPU acceleration if available (default: true)
|
|
41
41
|
* @param opts.projectionModelPath - Path to a multimodal projector file for vision models
|
|
42
|
+
* @param opts.draftModelPath - Optional MTP draft-heads gguf. Loading it lets `Chat`
|
|
43
|
+
* instances opt into MTP speculative decoding via `new Chat({ ..., mtp: true })`.
|
|
44
|
+
* Adds around 5% to VRAM usage.
|
|
42
45
|
* @param opts.onDownloadProgress - Invoked with `(downloaded, total)` byte counts while a remote model is being downloaded. Throttled to ~10 Hz with a guaranteed final emit on completion. Not invoked for cached/local files.
|
|
43
46
|
*/
|
|
44
47
|
static async load(opts: {
|
|
45
48
|
modelPath: string;
|
|
46
49
|
useGpu?: boolean;
|
|
47
50
|
projectionModelPath?: string;
|
|
51
|
+
draftModelPath?: string;
|
|
48
52
|
onDownloadProgress?: (downloaded: number, total: number) => void;
|
|
49
53
|
}): Promise<Model> {
|
|
50
54
|
const cb = opts.onDownloadProgress;
|
|
@@ -55,11 +59,20 @@ export class Model {
|
|
|
55
59
|
opts.modelPath,
|
|
56
60
|
opts.useGpu ?? true,
|
|
57
61
|
opts.projectionModelPath,
|
|
62
|
+
opts.draftModelPath,
|
|
58
63
|
progressCallback,
|
|
59
64
|
);
|
|
60
65
|
return new Model(inner);
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
/**
|
|
69
|
+
* The maximum context size this model was trained with.
|
|
70
|
+
* Use this to avoid setting `contextSize` above what the model supports.
|
|
71
|
+
*/
|
|
72
|
+
get maxCtx(): number {
|
|
73
|
+
return Number((this._inner as any).maxCtx());
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
/**
|
|
64
77
|
* Immediately free the underlying Rust resources (model weights, GPU memory).
|
|
65
78
|
* 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,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RustTts,
|
|
3
|
+
loadTts,
|
|
4
|
+
type RustTtsInterface,
|
|
5
|
+
} from "../generated/ts/nobodywho";
|
|
6
|
+
|
|
7
|
+
export type TtsArchitecture = "kokoro" | "pocket-tts" | "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
|
+
precision?: "int8" | "fp32";
|
|
19
|
+
temperature?: number;
|
|
20
|
+
huggingfaceToken?: string;
|
|
21
|
+
device?: TtsDevice;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Text-to-speech synthesizer that returns WAV bytes. */
|
|
25
|
+
export class Tts {
|
|
26
|
+
/** @internal */
|
|
27
|
+
private _inner: RustTtsInterface;
|
|
28
|
+
|
|
29
|
+
/** Create a TTS synthesizer synchronously. */
|
|
30
|
+
constructor(opts: TtsOptions) {
|
|
31
|
+
this._inner = new RustTts(
|
|
32
|
+
opts.source,
|
|
33
|
+
opts.architecture,
|
|
34
|
+
opts.voice,
|
|
35
|
+
opts.language,
|
|
36
|
+
opts.speed,
|
|
37
|
+
opts.steps,
|
|
38
|
+
opts.silenceDuration,
|
|
39
|
+
opts.precision,
|
|
40
|
+
opts.temperature,
|
|
41
|
+
opts.huggingfaceToken,
|
|
42
|
+
opts.device ?? "auto",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private static fromInner(inner: RustTtsInterface): Tts {
|
|
47
|
+
const tts = Object.create(Tts.prototype) as Tts;
|
|
48
|
+
tts._inner = inner;
|
|
49
|
+
return tts;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Create a TTS synthesizer asynchronously. */
|
|
53
|
+
static async load(opts: TtsOptions): Promise<Tts> {
|
|
54
|
+
const inner = await loadTts(
|
|
55
|
+
opts.source,
|
|
56
|
+
opts.architecture,
|
|
57
|
+
opts.voice,
|
|
58
|
+
opts.language,
|
|
59
|
+
opts.speed,
|
|
60
|
+
opts.steps,
|
|
61
|
+
opts.silenceDuration,
|
|
62
|
+
opts.precision,
|
|
63
|
+
opts.temperature,
|
|
64
|
+
opts.huggingfaceToken,
|
|
65
|
+
opts.device ?? "auto",
|
|
66
|
+
);
|
|
67
|
+
return Tts.fromInner(inner);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Synthesize text and return WAV bytes. */
|
|
71
|
+
async synthesize(text: string): Promise<Uint8Array> {
|
|
72
|
+
return new Uint8Array(await this._inner.synthesizeAsync(text));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Synthesize text synchronously and return WAV bytes. */
|
|
76
|
+
synthesizeSync(text: string): Uint8Array {
|
|
77
|
+
return new Uint8Array(this._inner.synthesize(text));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Immediately free the underlying Rust resources.
|
|
82
|
+
* After calling this, the Tts instance is no longer usable.
|
|
83
|
+
*/
|
|
84
|
+
destroy(): void {
|
|
85
|
+
(this._inner as any).uniffiDestroy();
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/wrapper.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
8
|
* - Utilities: SamplerPresets, cosineSimilarity, downloadModel
|
|
@@ -10,7 +10,23 @@
|
|
|
10
10
|
* This file is NOT generated — it is safe to edit.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
|
|
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();
|
|
14
30
|
|
|
15
31
|
/**
|
|
16
32
|
* Download a GGUF model from a remote URL or HuggingFace path and return the local file path.
|
|
@@ -41,8 +57,10 @@ export async function downloadModel(opts: {
|
|
|
41
57
|
// Wrapper classes
|
|
42
58
|
export { Model } from "./model";
|
|
43
59
|
export { Chat } from "./chat";
|
|
60
|
+
export { STT } from "./stt";
|
|
44
61
|
export { Encoder } from "./encoder";
|
|
45
62
|
export { CrossEncoder } from "./cross_encoder";
|
|
63
|
+
export { Tts } from "./tts";
|
|
46
64
|
export { Prompt } from "./prompt";
|
|
47
65
|
export { Tool } from "./tool";
|
|
48
66
|
export { TokenStream } from "./streaming";
|
|
@@ -59,13 +77,16 @@ export {
|
|
|
59
77
|
SamplerConfig,
|
|
60
78
|
cosineSimilarity,
|
|
61
79
|
getCachedModels,
|
|
62
|
-
} from "
|
|
80
|
+
} from "../generated/ts/nobodywho";
|
|
63
81
|
|
|
64
82
|
export type {
|
|
65
83
|
Asset,
|
|
66
84
|
ToolCall,
|
|
67
85
|
CachedModel,
|
|
68
|
-
|
|
86
|
+
ChatStats,
|
|
87
|
+
} from "../generated/ts/nobodywho";
|
|
88
|
+
export type { TtsArchitecture, TtsDevice, TtsOptions } from "./tts";
|
|
89
|
+
export type { SttOptions } from "./stt";
|
|
69
90
|
|
|
70
91
|
// Ergonomic wrapper additions.
|
|
71
92
|
export { SamplerPresets } from "./sampler_presets";
|