react-native-nobodywho 2.5.0 → 4.0.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/README.md +113 -7
- package/android/build.gradle +5 -1
- package/generated/cpp/nobodywho.cpp +1050 -315
- package/generated/cpp/nobodywho.hpp +127 -27
- package/generated/ts/nobodywho-ffi.ts +52 -27
- package/generated/ts/nobodywho.ts +1240 -499
- package/package.json +7 -3
- package/src/chat.ts +35 -0
- package/src/encoder.ts +5 -0
- package/src/message.ts +91 -24
- package/src/prompt.ts +8 -8
- package/src/{stt.ts → speech_to_text.ts} +17 -9
- package/src/{tts.ts → text_to_speech.ts} +20 -20
- package/src/vad.ts +104 -0
- package/src/wrapper.ts +11 -8
package/package.json
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-nobodywho",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Run LLMs locally
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "Run LLMs locally and offline with React Native & Expo - Tool calling, RAG, Speech-to-Text, Text-to-Speech, and VAD",
|
|
5
5
|
"main": "src/wrapper.ts",
|
|
6
6
|
"types": "src/wrapper.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/wrapper.ts"
|
|
9
9
|
},
|
|
10
10
|
"license": "EUPL-1.2",
|
|
11
|
-
"homepage": "https://nobodywho.
|
|
11
|
+
"homepage": "https://www.nobodywho.ai",
|
|
12
12
|
"bugs": "https://github.com/nobodywho-ooo/nobodywho/issues",
|
|
13
13
|
"keywords": [
|
|
14
14
|
"react-native",
|
|
15
|
+
"expo",
|
|
15
16
|
"ai",
|
|
16
17
|
"llm",
|
|
17
18
|
"local-ai",
|
|
18
19
|
"inference",
|
|
20
|
+
"stt",
|
|
21
|
+
"tts",
|
|
22
|
+
"vad",
|
|
19
23
|
"gguf",
|
|
20
24
|
"on-device",
|
|
21
25
|
"offline"
|
package/src/chat.ts
CHANGED
|
@@ -10,6 +10,18 @@ import { TokenStream } from "./streaming";
|
|
|
10
10
|
import type { Prompt } from "./prompt";
|
|
11
11
|
import type { Tool } from "./tool";
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Settings to apply before a `complete` turn. An omitted field keeps what the
|
|
15
|
+
* chat has; a set one stays set, like a leading system message.
|
|
16
|
+
*/
|
|
17
|
+
export type Options = {
|
|
18
|
+
sampler?: SamplerConfig;
|
|
19
|
+
/** Replaces the chat's template variables wholesale. */
|
|
20
|
+
templateVariables?: Record<string, boolean>;
|
|
21
|
+
/** Re-selects the chat template, so the turn re-prefills from near token zero. */
|
|
22
|
+
tools?: Tool[];
|
|
23
|
+
};
|
|
24
|
+
|
|
13
25
|
/**
|
|
14
26
|
* A chat session for local LLM inference.
|
|
15
27
|
*
|
|
@@ -107,6 +119,29 @@ export class Chat {
|
|
|
107
119
|
return new TokenStream(this._inner.askWithPrompt(message._parts!));
|
|
108
120
|
}
|
|
109
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Answer a full list of messages, replacing the chat history.
|
|
124
|
+
*
|
|
125
|
+
* The list is the whole conversation, used as given: it must be non-empty, end
|
|
126
|
+
* in a user or tool message, and carry a system message only first. That system
|
|
127
|
+
* message sets the chat's system prompt; leave it out and the prompt already on
|
|
128
|
+
* the chat is kept. The response is appended, and the next `ask` continues from
|
|
129
|
+
* there.
|
|
130
|
+
*
|
|
131
|
+
* `options` follows the same rule for the chat's other settings.
|
|
132
|
+
*/
|
|
133
|
+
complete(messages: Message[], options: Options = {}): TokenStream {
|
|
134
|
+
return new TokenStream(
|
|
135
|
+
this._inner.complete(messages.map(toInternal), {
|
|
136
|
+
sampler: options.sampler,
|
|
137
|
+
templateVariables: options.templateVariables
|
|
138
|
+
? new Map(Object.entries(options.templateVariables))
|
|
139
|
+
: undefined,
|
|
140
|
+
tools: options.tools?.map((t) => t._inner),
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
110
145
|
/** Stop the current generation. */
|
|
111
146
|
stopGeneration(): void {
|
|
112
147
|
this._inner.stopGeneration();
|
package/src/encoder.ts
CHANGED
|
@@ -46,6 +46,11 @@ export class Encoder {
|
|
|
46
46
|
return this._inner.encode(text);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** Encode multiple texts, preserving input order. */
|
|
50
|
+
async encodeBatch(texts: string[]): Promise<number[][]> {
|
|
51
|
+
return this._inner.encodeBatch(texts);
|
|
52
|
+
}
|
|
53
|
+
|
|
49
54
|
/**
|
|
50
55
|
* Immediately free the underlying Rust resources.
|
|
51
56
|
* After calling this, the Encoder instance is no longer usable.
|
package/src/message.ts
CHANGED
|
@@ -1,19 +1,45 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ContentPart as InternalContentPart,
|
|
3
|
+
ContentPart_Tags,
|
|
2
4
|
Message as InternalMessage,
|
|
3
5
|
Message_Tags,
|
|
4
|
-
|
|
6
|
+
MessageContent as InternalMessageContent,
|
|
7
|
+
MessageContent_Tags,
|
|
5
8
|
type ToolCall,
|
|
6
9
|
} from "../generated/ts/nobodywho";
|
|
7
10
|
|
|
11
|
+
/** One piece of a message: a run of text, or a media file at this position. */
|
|
12
|
+
export type ContentPart =
|
|
13
|
+
| { type: "text"; text: string }
|
|
14
|
+
| { type: "image"; path: string }
|
|
15
|
+
| { type: "audio"; path: string };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Message content: plain text, interleaved text and media, or a raw JSON value
|
|
19
|
+
* handed straight to the chat template.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const content: Content = [
|
|
24
|
+
* { type: "text", text: "What is in this image?" },
|
|
25
|
+
* { type: "image", path: "./dog.png" },
|
|
26
|
+
* ];
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export type Content = string | ContentPart[] | { type: "json"; json: string };
|
|
30
|
+
|
|
8
31
|
/**
|
|
9
32
|
* A chat message. The variant determines the message type:
|
|
10
33
|
*
|
|
11
|
-
* - **User message:** `{ role: "user", content
|
|
34
|
+
* - **User message:** `{ role: "user", content }`
|
|
12
35
|
* - **Assistant message:** `{ role: "assistant", content }`
|
|
13
36
|
* - **Assistant tool call:** `{ role: "assistant", content, toolCalls }`
|
|
14
37
|
* - **System message:** `{ role: "system", content }`
|
|
15
38
|
* - **Tool response:** `{ role: "tool", name, content }`
|
|
16
39
|
*
|
|
40
|
+
* `content` is a string, a list of parts for multimodal input, or a
|
|
41
|
+
* `{ type: "json", json }` wrapper for raw template passthrough.
|
|
42
|
+
*
|
|
17
43
|
* @example
|
|
18
44
|
* ```typescript
|
|
19
45
|
* const history = await chat.getChatHistory();
|
|
@@ -29,33 +55,75 @@ import {
|
|
|
29
55
|
* ```
|
|
30
56
|
*/
|
|
31
57
|
export type Message =
|
|
32
|
-
| { role: "user"; content:
|
|
33
|
-
| { role: "assistant"; content:
|
|
34
|
-
| { role: "assistant"; content:
|
|
35
|
-
| { role: "system"; content:
|
|
36
|
-
| { role: "tool"; name: string; content:
|
|
58
|
+
| { role: "user"; content: Content }
|
|
59
|
+
| { role: "assistant"; content: Content }
|
|
60
|
+
| { role: "assistant"; content: Content; toolCalls: ToolCall[] }
|
|
61
|
+
| { role: "system"; content: Content }
|
|
62
|
+
| { role: "tool"; name: string; content: Content };
|
|
63
|
+
|
|
64
|
+
function partFromInternal(part: InternalContentPart): ContentPart {
|
|
65
|
+
if (part.tag === ContentPart_Tags.Text) {
|
|
66
|
+
return { type: "text", text: part.inner.text };
|
|
67
|
+
} else if (part.tag === ContentPart_Tags.Image) {
|
|
68
|
+
return { type: "image", path: part.inner.path };
|
|
69
|
+
} else {
|
|
70
|
+
return { type: "audio", path: part.inner.path };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function partToInternal(part: ContentPart): InternalContentPart {
|
|
75
|
+
if (part.type === "text") {
|
|
76
|
+
return new InternalContentPart.Text({ text: part.text });
|
|
77
|
+
} else if (part.type === "image") {
|
|
78
|
+
return new InternalContentPart.Image({ path: part.path });
|
|
79
|
+
} else {
|
|
80
|
+
return new InternalContentPart.Audio({ path: part.path });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** @internal */
|
|
85
|
+
export function contentFromInternal(content: InternalMessageContent): Content {
|
|
86
|
+
if (content.tag === MessageContent_Tags.Text) {
|
|
87
|
+
return content.inner.text;
|
|
88
|
+
} else if (content.tag === MessageContent_Tags.Parts) {
|
|
89
|
+
return content.inner.parts.map(partFromInternal);
|
|
90
|
+
} else {
|
|
91
|
+
return { type: "json", json: content.inner.json };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @internal */
|
|
96
|
+
export function contentToInternal(content: Content): InternalMessageContent {
|
|
97
|
+
if (typeof content === "string") {
|
|
98
|
+
return new InternalMessageContent.Text({ text: content });
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(content)) {
|
|
101
|
+
return new InternalMessageContent.Parts({
|
|
102
|
+
parts: content.map(partToInternal),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return new InternalMessageContent.Json({ json: content.json });
|
|
106
|
+
}
|
|
37
107
|
|
|
38
108
|
/** @internal Convert internal Message to Message */
|
|
39
109
|
export function fromInternal(msg: InternalMessage): Message {
|
|
40
110
|
if (msg.tag === Message_Tags.User) {
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
role: "user",
|
|
44
|
-
content,
|
|
45
|
-
...(assets.length > 0 ? { assets } : {}),
|
|
46
|
-
};
|
|
111
|
+
return { role: "user", content: contentFromInternal(msg.inner.content) };
|
|
47
112
|
} else if (msg.tag === Message_Tags.Assistant) {
|
|
48
113
|
const { content, toolCalls } = msg.inner;
|
|
49
114
|
if (toolCalls != null && toolCalls.length > 0) {
|
|
50
|
-
return {
|
|
115
|
+
return {
|
|
116
|
+
role: "assistant",
|
|
117
|
+
content: contentFromInternal(content),
|
|
118
|
+
toolCalls,
|
|
119
|
+
};
|
|
51
120
|
}
|
|
52
|
-
return { role: "assistant", content };
|
|
121
|
+
return { role: "assistant", content: contentFromInternal(content) };
|
|
53
122
|
} else if (msg.tag === Message_Tags.System) {
|
|
54
|
-
|
|
55
|
-
return { role: "system", content };
|
|
123
|
+
return { role: "system", content: contentFromInternal(msg.inner.content) };
|
|
56
124
|
} else {
|
|
57
125
|
const { name, content } = msg.inner;
|
|
58
|
-
return { role: "tool", name, content };
|
|
126
|
+
return { role: "tool", name, content: contentFromInternal(content) };
|
|
59
127
|
}
|
|
60
128
|
}
|
|
61
129
|
|
|
@@ -63,27 +131,26 @@ export function fromInternal(msg: InternalMessage): Message {
|
|
|
63
131
|
export function toInternal(msg: Message): InternalMessage {
|
|
64
132
|
if (msg.role === "user") {
|
|
65
133
|
return new InternalMessage.User({
|
|
66
|
-
content: msg.content,
|
|
67
|
-
assets: msg.assets ?? [],
|
|
134
|
+
content: contentToInternal(msg.content),
|
|
68
135
|
});
|
|
69
136
|
} else if (msg.role === "assistant" && "toolCalls" in msg) {
|
|
70
137
|
return new InternalMessage.Assistant({
|
|
71
|
-
content: msg.content,
|
|
138
|
+
content: contentToInternal(msg.content),
|
|
72
139
|
toolCalls: msg.toolCalls,
|
|
73
140
|
});
|
|
74
141
|
} else if (msg.role === "assistant") {
|
|
75
142
|
return new InternalMessage.Assistant({
|
|
76
|
-
content: msg.content,
|
|
143
|
+
content: contentToInternal(msg.content),
|
|
77
144
|
toolCalls: undefined,
|
|
78
145
|
});
|
|
79
146
|
} else if (msg.role === "system") {
|
|
80
147
|
return new InternalMessage.System({
|
|
81
|
-
content: msg.content,
|
|
148
|
+
content: contentToInternal(msg.content),
|
|
82
149
|
});
|
|
83
150
|
} else {
|
|
84
151
|
return new InternalMessage.Tool({
|
|
85
152
|
name: msg.name,
|
|
86
|
-
content: msg.content,
|
|
153
|
+
content: contentToInternal(msg.content),
|
|
87
154
|
});
|
|
88
155
|
}
|
|
89
156
|
}
|
package/src/prompt.ts
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ContentPart } from "../generated/ts/nobodywho";
|
|
2
2
|
|
|
3
3
|
/** A text part of a multimodal prompt. */
|
|
4
4
|
class TextPart {
|
|
5
5
|
/** @internal */
|
|
6
|
-
readonly _inner:
|
|
6
|
+
readonly _inner: ContentPart;
|
|
7
7
|
|
|
8
8
|
constructor(content: string) {
|
|
9
|
-
this._inner = new
|
|
9
|
+
this._inner = new ContentPart.Text({ text: content });
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/** An image part of a multimodal prompt. */
|
|
14
14
|
class ImagePart {
|
|
15
15
|
/** @internal */
|
|
16
|
-
readonly _inner:
|
|
16
|
+
readonly _inner: ContentPart;
|
|
17
17
|
|
|
18
18
|
constructor(path: string) {
|
|
19
|
-
this._inner = new
|
|
19
|
+
this._inner = new ContentPart.Image({ path });
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/** An audio part of a multimodal prompt. */
|
|
24
24
|
class AudioPart {
|
|
25
25
|
/** @internal */
|
|
26
|
-
readonly _inner:
|
|
26
|
+
readonly _inner: ContentPart;
|
|
27
27
|
|
|
28
28
|
constructor(path: string) {
|
|
29
|
-
this._inner = new
|
|
29
|
+
this._inner = new ContentPart.Audio({ path });
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -53,7 +53,7 @@ type Part = TextPart | ImagePart | AudioPart;
|
|
|
53
53
|
*/
|
|
54
54
|
export class Prompt {
|
|
55
55
|
/** @internal */
|
|
56
|
-
readonly _parts:
|
|
56
|
+
readonly _parts: ContentPart[] | null;
|
|
57
57
|
/** @internal */
|
|
58
58
|
readonly _jsonString: string | null;
|
|
59
59
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RustSpeechToTextInterface } from "../generated/ts/nobodywho";
|
|
2
2
|
import * as nobodywho from "../generated/ts/nobodywho";
|
|
3
3
|
import { TokenStream } from "./streaming";
|
|
4
4
|
|
|
5
|
-
export type
|
|
5
|
+
export type SpeechToTextOptions = {
|
|
6
6
|
source: string;
|
|
7
7
|
language?: string;
|
|
8
8
|
quantization?: string;
|
|
@@ -13,7 +13,7 @@ export type SttOptions = {
|
|
|
13
13
|
*
|
|
14
14
|
* @example
|
|
15
15
|
* ```typescript
|
|
16
|
-
* const stt =
|
|
16
|
+
* const stt = await SpeechToText.load({
|
|
17
17
|
* source: "hf://onnx-community/whisper-base",
|
|
18
18
|
* language: "en",
|
|
19
19
|
* });
|
|
@@ -26,24 +26,32 @@ export type SttOptions = {
|
|
|
26
26
|
* const text = await stream.completed();
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
|
-
export class
|
|
29
|
+
export class SpeechToText {
|
|
30
30
|
/** @internal */
|
|
31
|
-
private readonly _inner:
|
|
31
|
+
private readonly _inner: RustSpeechToTextInterface;
|
|
32
|
+
|
|
33
|
+
/** @internal */
|
|
34
|
+
private constructor(inner: RustSpeechToTextInterface) {
|
|
35
|
+
this._inner = inner;
|
|
36
|
+
}
|
|
32
37
|
|
|
33
38
|
/**
|
|
34
|
-
*
|
|
39
|
+
* Load a Whisper SpeechToText model.
|
|
40
|
+
*
|
|
41
|
+
* @param opts - See {@link SpeechToTextOptions}.
|
|
35
42
|
*/
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
static async load(opts: SpeechToTextOptions): Promise<SpeechToText> {
|
|
44
|
+
const inner = await nobodywho.loadSpeechToText(
|
|
38
45
|
opts.source,
|
|
39
46
|
opts.language,
|
|
40
47
|
opts.quantization,
|
|
41
48
|
);
|
|
49
|
+
return new SpeechToText(inner);
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
/**
|
|
45
53
|
* Transcribe an audio file (WAV / MP3).
|
|
46
|
-
* Returns an `
|
|
54
|
+
* Returns an `SpeechToTextStream` to consume tokens as they arrive.
|
|
47
55
|
*/
|
|
48
56
|
transcribeFile(path: string): TokenStream {
|
|
49
57
|
return new TokenStream(this._inner.transcribeFile(path));
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type
|
|
2
|
+
RustTextToSpeech,
|
|
3
|
+
loadTextToSpeech,
|
|
4
|
+
type RustTextToSpeechInterface,
|
|
5
5
|
} from "../generated/ts/nobodywho";
|
|
6
6
|
|
|
7
|
-
export type
|
|
8
|
-
export type
|
|
7
|
+
export type TextToSpeechArchitecture = "kokoro" | "pocket-tts" | "supertonic";
|
|
8
|
+
export type TextToSpeechDevice = "auto" | "cpu" | "cuda";
|
|
9
9
|
|
|
10
|
-
export type
|
|
10
|
+
export type TextToSpeechOptions = {
|
|
11
11
|
source: string;
|
|
12
|
-
architecture?:
|
|
12
|
+
architecture?: TextToSpeechArchitecture;
|
|
13
13
|
voice?: string;
|
|
14
14
|
language?: string;
|
|
15
15
|
speed?: number;
|
|
@@ -18,17 +18,17 @@ export type TtsOptions = {
|
|
|
18
18
|
precision?: "int8" | "fp32";
|
|
19
19
|
temperature?: number;
|
|
20
20
|
huggingfaceToken?: string;
|
|
21
|
-
device?:
|
|
21
|
+
device?: TextToSpeechDevice;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
/** Text-to-speech synthesizer that returns WAV bytes. */
|
|
25
|
-
export class
|
|
25
|
+
export class TextToSpeech {
|
|
26
26
|
/** @internal */
|
|
27
|
-
private _inner:
|
|
27
|
+
private _inner: RustTextToSpeechInterface;
|
|
28
28
|
|
|
29
|
-
/** Create a
|
|
30
|
-
constructor(opts:
|
|
31
|
-
this._inner = new
|
|
29
|
+
/** Create a TextToSpeech synthesizer synchronously. */
|
|
30
|
+
constructor(opts: TextToSpeechOptions) {
|
|
31
|
+
this._inner = new RustTextToSpeech(
|
|
32
32
|
opts.source,
|
|
33
33
|
opts.architecture,
|
|
34
34
|
opts.voice,
|
|
@@ -43,15 +43,15 @@ export class Tts {
|
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
private static fromInner(inner:
|
|
47
|
-
const tts = Object.create(
|
|
46
|
+
private static fromInner(inner: RustTextToSpeechInterface): TextToSpeech {
|
|
47
|
+
const tts = Object.create(TextToSpeech.prototype) as TextToSpeech;
|
|
48
48
|
tts._inner = inner;
|
|
49
49
|
return tts;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/** Create a
|
|
53
|
-
static async load(opts:
|
|
54
|
-
const inner = await
|
|
52
|
+
/** Create a TextToSpeech synthesizer asynchronously. */
|
|
53
|
+
static async load(opts: TextToSpeechOptions): Promise<TextToSpeech> {
|
|
54
|
+
const inner = await loadTextToSpeech(
|
|
55
55
|
opts.source,
|
|
56
56
|
opts.architecture,
|
|
57
57
|
opts.voice,
|
|
@@ -64,7 +64,7 @@ export class Tts {
|
|
|
64
64
|
opts.huggingfaceToken,
|
|
65
65
|
opts.device ?? "auto",
|
|
66
66
|
);
|
|
67
|
-
return
|
|
67
|
+
return TextToSpeech.fromInner(inner);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/** Synthesize text and return WAV bytes. */
|
|
@@ -79,7 +79,7 @@ export class Tts {
|
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
81
|
* Immediately free the underlying Rust resources.
|
|
82
|
-
* After calling this, the
|
|
82
|
+
* After calling this, the TextToSpeech instance is no longer usable.
|
|
83
83
|
*/
|
|
84
84
|
destroy(): void {
|
|
85
85
|
(this._inner as any).uniffiDestroy();
|
package/src/vad.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { RustVoiceActivityDetectionInterface, VoiceActivityDetectionEvent } from "../generated/ts/nobodywho";
|
|
2
|
+
import * as nobodywho from "../generated/ts/nobodywho";
|
|
3
|
+
|
|
4
|
+
export { VoiceActivityDetectionEvent } from "../generated/ts/nobodywho";
|
|
5
|
+
|
|
6
|
+
export type VoiceActivityDetectionOptions = {
|
|
7
|
+
source?: string;
|
|
8
|
+
sampleRate: number;
|
|
9
|
+
threshold?: number;
|
|
10
|
+
minSilenceDurationMs?: number;
|
|
11
|
+
minSpeechDurationMs?: number;
|
|
12
|
+
prerollDurationMs?: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Voice activity detection from live, streaming audio, backed by Silero VAD.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const vad = await VoiceActivityDetection.load({ sampleRate: 16000 });
|
|
21
|
+
*
|
|
22
|
+
* // Feed each newest chunk as it arrives (not the whole buffer — VoiceActivityDetection
|
|
23
|
+
* // tracks the current turn internally).
|
|
24
|
+
* if (vad.push(chunk) === VoiceActivityDetectionEvent.SpeechEnded) {
|
|
25
|
+
* const audio = vad.finish();
|
|
26
|
+
* // audio: Int16Array-like number[] spanning SpeechStarted (with a
|
|
27
|
+
* // small pre-roll) through SpeechEnded.
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export class VoiceActivityDetection {
|
|
32
|
+
/** @internal */
|
|
33
|
+
private readonly _inner: RustVoiceActivityDetectionInterface;
|
|
34
|
+
|
|
35
|
+
/** @internal */
|
|
36
|
+
private constructor(inner: RustVoiceActivityDetectionInterface) {
|
|
37
|
+
this._inner = inner;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Load a voice activity detector.
|
|
42
|
+
*
|
|
43
|
+
* @param opts - See {@link VoiceActivityDetectionOptions}.
|
|
44
|
+
*/
|
|
45
|
+
static async load(opts: VoiceActivityDetectionOptions): Promise<VoiceActivityDetection> {
|
|
46
|
+
const inner = await nobodywho.loadVoiceActivityDetection(
|
|
47
|
+
opts.source,
|
|
48
|
+
opts.sampleRate,
|
|
49
|
+
opts.threshold,
|
|
50
|
+
opts.minSilenceDurationMs,
|
|
51
|
+
opts.minSpeechDurationMs,
|
|
52
|
+
opts.prerollDurationMs,
|
|
53
|
+
undefined,
|
|
54
|
+
);
|
|
55
|
+
return new VoiceActivityDetection(inner);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Feed the newest chunk of audio (not the whole accumulated buffer —
|
|
60
|
+
* `VoiceActivityDetection` tracks the current turn internally). Always
|
|
61
|
+
* returns the current confirmed state: `Speech`/`Silence` if unchanged
|
|
62
|
+
* since the last call, or `SpeechStarted`/`SpeechEnded` on the call that
|
|
63
|
+
* confirmed the transition.
|
|
64
|
+
*
|
|
65
|
+
* @param chunk - Flat array of signed 16-bit samples (mono).
|
|
66
|
+
*/
|
|
67
|
+
push(chunk: Int16Array | number[]): VoiceActivityDetectionEvent {
|
|
68
|
+
const arr = chunk instanceof Int16Array ? Array.from(chunk) : chunk;
|
|
69
|
+
return this._inner.push(arr);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Return the current turn's captured audio (from the confirmed
|
|
74
|
+
* `SpeechStarted`, including a small pre-roll, through to `SpeechEnded`)
|
|
75
|
+
* and reset internal state for the next turn. Empty if speech was never
|
|
76
|
+
* confirmed.
|
|
77
|
+
*/
|
|
78
|
+
finish(): number[] {
|
|
79
|
+
return this._inner.finish();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Detect every speech segment in a complete audio buffer, returning
|
|
84
|
+
* each segment's audio (with a short pre-roll) in order. Unlike `push`,
|
|
85
|
+
* correctly finds every segment regardless of buffer size — use this
|
|
86
|
+
* for offline/batch processing instead of live streaming.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```typescript
|
|
90
|
+
* for (const audio of vad.segment(fullRecording)) {
|
|
91
|
+
* transcribe(audio);
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
segment(samples: Int16Array | number[]): number[][] {
|
|
96
|
+
const arr = samples instanceof Int16Array ? Array.from(samples) : samples;
|
|
97
|
+
return this._inner.segment(arr);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Release native resources. Call when done with this handle. */
|
|
101
|
+
destroy(): void {
|
|
102
|
+
(this._inner as any).uniffiDestroy?.();
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/wrapper.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
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,
|
|
5
|
+
* - Wrapper classes: Model, Chat, Encoder, CrossEncoder, TextToSpeech, Tool, TokenStream, Prompt
|
|
6
6
|
* - Direct re-exports: SamplerBuilder, SamplerConfig
|
|
7
|
-
* - Types: Message,
|
|
7
|
+
* - Types: Message, Content, ContentPart, ToolCall
|
|
8
8
|
* - Utilities: SamplerPresets, cosineSimilarity, downloadModel
|
|
9
9
|
*
|
|
10
10
|
* This file is NOT generated — it is safe to edit.
|
|
@@ -57,16 +57,18 @@ export async function downloadModel(opts: {
|
|
|
57
57
|
// Wrapper classes
|
|
58
58
|
export { Model } from "./model";
|
|
59
59
|
export { Chat } from "./chat";
|
|
60
|
-
export {
|
|
60
|
+
export type { Options } from "./chat";
|
|
61
|
+
export { SpeechToText } from "./speech_to_text";
|
|
62
|
+
export { VoiceActivityDetection } from "./vad";
|
|
61
63
|
export { Encoder } from "./encoder";
|
|
62
64
|
export { CrossEncoder } from "./cross_encoder";
|
|
63
|
-
export {
|
|
65
|
+
export { TextToSpeech } from "./text_to_speech";
|
|
64
66
|
export { Prompt } from "./prompt";
|
|
65
67
|
export { Tool } from "./tool";
|
|
66
68
|
export { TokenStream } from "./streaming";
|
|
67
69
|
|
|
68
70
|
// Message types
|
|
69
|
-
export type { Message } from "./message";
|
|
71
|
+
export type { Message, Content, ContentPart } from "./message";
|
|
70
72
|
|
|
71
73
|
// Re-export types that don't need wrapping.
|
|
72
74
|
//
|
|
@@ -80,13 +82,14 @@ export {
|
|
|
80
82
|
} from "../generated/ts/nobodywho";
|
|
81
83
|
|
|
82
84
|
export type {
|
|
83
|
-
Asset,
|
|
84
85
|
ToolCall,
|
|
85
86
|
CachedModel,
|
|
86
87
|
ChatStats,
|
|
87
88
|
} from "../generated/ts/nobodywho";
|
|
88
|
-
export type {
|
|
89
|
-
export type {
|
|
89
|
+
export type { TextToSpeechArchitecture, TextToSpeechDevice, TextToSpeechOptions } from "./text_to_speech";
|
|
90
|
+
export type { SpeechToTextOptions } from "./speech_to_text";
|
|
91
|
+
export { VoiceActivityDetectionEvent } from "./vad";
|
|
92
|
+
export type { VoiceActivityDetectionOptions } from "./vad";
|
|
90
93
|
|
|
91
94
|
// Ergonomic wrapper additions.
|
|
92
95
|
export { SamplerPresets } from "./sampler_presets";
|