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/generated/cpp/nobodywho.cpp +1093 -28
- package/generated/cpp/nobodywho.hpp +131 -0
- package/generated/ts/nobodywho-ffi.ts +41 -0
- package/generated/ts/nobodywho.ts +935 -22
- package/package.json +1 -1
- package/src/chat.ts +19 -1
- package/src/message.ts +1 -1
- package/src/model.ts +8 -0
- package/src/prompt.ts +15 -1
- package/src/stt.ts +69 -0
- package/src/tts.ts +78 -0
- package/src/wrapper.ts +58 -4
package/package.json
CHANGED
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
|
-
|
|
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:
|
|
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
|
-
|
|
79
|
+
getCachedModels,
|
|
80
|
+
} from "../generated/ts/nobodywho";
|
|
31
81
|
|
|
32
82
|
export type {
|
|
33
83
|
Asset,
|
|
34
84
|
ToolCall,
|
|
35
|
-
|
|
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";
|