@xybrid/react-native 0.10.0-rc1
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/LICENSE +201 -0
- package/README.md +298 -0
- package/android/build.gradle +61 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/ai/xybrid/reactnative/XybridCodec.kt +481 -0
- package/android/src/main/java/ai/xybrid/reactnative/XybridHandles.kt +139 -0
- package/android/src/main/java/ai/xybrid/reactnative/XybridModule.kt +665 -0
- package/android/src/main/java/ai/xybrid/reactnative/XybridPackage.kt +27 -0
- package/ios/XybridCodec.swift +508 -0
- package/ios/XybridHandles.swift +191 -0
- package/ios/XybridModule.mm +494 -0
- package/ios/XybridModuleImpl.swift +887 -0
- package/ios/XybridSwift/Xybrid.swift +1401 -0
- package/ios/XybridSwift/xybrid_bolt.swift +3198 -0
- package/ios/xybrid_natives.rb +178 -0
- package/lib/commonjs/NativeXybrid.js +24 -0
- package/lib/commonjs/base64.js +45 -0
- package/lib/commonjs/cancel.js +35 -0
- package/lib/commonjs/context.js +67 -0
- package/lib/commonjs/envelope.js +79 -0
- package/lib/commonjs/errors.js +61 -0
- package/lib/commonjs/index.js +59 -0
- package/lib/commonjs/model.js +307 -0
- package/lib/commonjs/pipeline.js +55 -0
- package/lib/commonjs/presets.js +32 -0
- package/lib/commonjs/session.js +76 -0
- package/lib/commonjs/types.js +10 -0
- package/lib/commonjs/wire.js +285 -0
- package/lib/commonjs/xybrid.js +133 -0
- package/lib/module/NativeXybrid.js +22 -0
- package/lib/module/base64.js +41 -0
- package/lib/module/cancel.js +29 -0
- package/lib/module/context.js +60 -0
- package/lib/module/envelope.js +73 -0
- package/lib/module/errors.js +56 -0
- package/lib/module/index.js +36 -0
- package/lib/module/model.js +298 -0
- package/lib/module/pipeline.js +48 -0
- package/lib/module/presets.js +27 -0
- package/lib/module/session.js +69 -0
- package/lib/module/types.js +9 -0
- package/lib/module/wire.js +271 -0
- package/lib/module/xybrid.js +127 -0
- package/lib/typescript/NativeXybrid.d.ts +68 -0
- package/lib/typescript/NativeXybrid.d.ts.map +1 -0
- package/lib/typescript/base64.d.ts +5 -0
- package/lib/typescript/base64.d.ts.map +1 -0
- package/lib/typescript/cancel.d.ts +24 -0
- package/lib/typescript/cancel.d.ts.map +1 -0
- package/lib/typescript/context.d.ts +40 -0
- package/lib/typescript/context.d.ts.map +1 -0
- package/lib/typescript/envelope.d.ts +51 -0
- package/lib/typescript/envelope.d.ts.map +1 -0
- package/lib/typescript/errors.d.ts +12 -0
- package/lib/typescript/errors.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +28 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/model.d.ts +211 -0
- package/lib/typescript/model.d.ts.map +1 -0
- package/lib/typescript/pipeline.d.ts +35 -0
- package/lib/typescript/pipeline.d.ts.map +1 -0
- package/lib/typescript/presets.d.ts +21 -0
- package/lib/typescript/presets.d.ts.map +1 -0
- package/lib/typescript/session.d.ts +50 -0
- package/lib/typescript/session.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +313 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/wire.d.ts +130 -0
- package/lib/typescript/wire.d.ts.map +1 -0
- package/lib/typescript/xybrid.d.ts +89 -0
- package/lib/typescript/xybrid.d.ts.map +1 -0
- package/package.json +88 -0
- package/react-native-xybrid.podspec +50 -0
- package/src/NativeXybrid.ts +131 -0
- package/src/base64.ts +43 -0
- package/src/cancel.ts +47 -0
- package/src/context.ts +70 -0
- package/src/envelope.ts +115 -0
- package/src/errors.ts +67 -0
- package/src/index.ts +91 -0
- package/src/model.ts +408 -0
- package/src/pipeline.ts +66 -0
- package/src/presets.ts +36 -0
- package/src/session.ts +76 -0
- package/src/types.ts +362 -0
- package/src/wire.ts +408 -0
- package/src/xybrid.ts +176 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Every native rejection carries one of these codes as `error.code`. The
|
|
2
|
+
// SDK codes map 1:1 onto bolt's `XybridError` variants (same order as
|
|
3
|
+
// crates/xybrid-bolt/src/lib.rs); the last three are raised by the React
|
|
4
|
+
// Native layer itself.
|
|
5
|
+
/** Stable error codes for `XybridError.code`. */
|
|
6
|
+
export const XybridErrorCodes = [
|
|
7
|
+
'xybrid_model_not_found',
|
|
8
|
+
'xybrid_directory_not_found',
|
|
9
|
+
'xybrid_metadata_not_found',
|
|
10
|
+
'xybrid_metadata_invalid',
|
|
11
|
+
'xybrid_load_error',
|
|
12
|
+
'xybrid_inference_error',
|
|
13
|
+
'xybrid_aborted_cloud_fallback',
|
|
14
|
+
'xybrid_streaming_unsupported',
|
|
15
|
+
'xybrid_not_loaded',
|
|
16
|
+
'xybrid_config_error',
|
|
17
|
+
'xybrid_network_error',
|
|
18
|
+
'xybrid_offline',
|
|
19
|
+
'xybrid_io_error',
|
|
20
|
+
'xybrid_cache_error',
|
|
21
|
+
'xybrid_pipeline_error',
|
|
22
|
+
'xybrid_circuit_open',
|
|
23
|
+
'xybrid_rate_limited',
|
|
24
|
+
'xybrid_timeout',
|
|
25
|
+
'xybrid_missing_artifact',
|
|
26
|
+
'xybrid_unsupported_model_capability',
|
|
27
|
+
'xybrid_unsupported_backend_capability',
|
|
28
|
+
'xybrid_invalid_image',
|
|
29
|
+
'xybrid_cancelled',
|
|
30
|
+
// React Native layer:
|
|
31
|
+
/** The handle was never issued, was released, or names another kind of object. */
|
|
32
|
+
'xybrid_handle',
|
|
33
|
+
/** A JS argument could not be decoded (bad envelope, options, source, base64…). */
|
|
34
|
+
'xybrid_invalid_argument',
|
|
35
|
+
/** Anything unexpected; the message carries the detail. */
|
|
36
|
+
'xybrid_unknown',
|
|
37
|
+
];
|
|
38
|
+
const CODES = new Set(XybridErrorCodes);
|
|
39
|
+
/** Narrow an unknown rejection to a {@link XybridError}. */
|
|
40
|
+
export function isXybridError(error) {
|
|
41
|
+
if (!(error instanceof Error))
|
|
42
|
+
return false;
|
|
43
|
+
const { code } = error;
|
|
44
|
+
return typeof code === 'string' && CODES.has(code);
|
|
45
|
+
}
|
|
46
|
+
// Mirrors `RetryableError::is_retryable` on the Rust `SdkError`.
|
|
47
|
+
const RETRYABLE = new Set([
|
|
48
|
+
'xybrid_network_error',
|
|
49
|
+
'xybrid_rate_limited',
|
|
50
|
+
'xybrid_timeout',
|
|
51
|
+
'xybrid_offline',
|
|
52
|
+
]);
|
|
53
|
+
/** Whether retrying the same call later can succeed (network, rate limit, timeout, offline). */
|
|
54
|
+
export function isRetryable(error) {
|
|
55
|
+
return isXybridError(error) && RETRYABLE.has(error.code);
|
|
56
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @xybrid/react-native — on-device (and optionally cloud) inference for React
|
|
2
|
+
// Native, backed by the same Rust SDK as the Swift, Kotlin, Flutter and Unity
|
|
3
|
+
// bindings.
|
|
4
|
+
//
|
|
5
|
+
// await Xybrid.initialize({ apiKey }); // optional
|
|
6
|
+
// const model = await ModelLoader.fromRegistry('qwen3.5-0.8b').load();
|
|
7
|
+
// const result = await model.run(Envelope.text('Hello'));
|
|
8
|
+
// await model.release();
|
|
9
|
+
import NativeXybrid from './NativeXybrid';
|
|
10
|
+
export { Xybrid } from './xybrid';
|
|
11
|
+
export { Model, ModelDownload, ModelLoader, } from './model';
|
|
12
|
+
export { ConversationContext } from './context';
|
|
13
|
+
export { Pipeline } from './pipeline';
|
|
14
|
+
export { StreamingSession } from './session';
|
|
15
|
+
export { Envelope } from './envelope';
|
|
16
|
+
export { GenerationConfigs, creative, greedy } from './presets';
|
|
17
|
+
export { XybridErrorCodes, isRetryable, isXybridError, } from './errors';
|
|
18
|
+
export { bytesToBase64, float32ToBase64 } from './base64';
|
|
19
|
+
/**
|
|
20
|
+
* Convert a JSON Schema (object or JSON string) into a GBNF grammar for
|
|
21
|
+
* `generationConfig.grammar`, so a local LLM emits schema-valid JSON. Runs the
|
|
22
|
+
* same native converter every binding uses; rejects with
|
|
23
|
+
* `xybrid_config_error` on invalid JSON or an unsupported construct.
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* const grammar = await jsonSchemaToGbnf({
|
|
27
|
+
* type: 'object',
|
|
28
|
+
* properties: { name: { type: 'string' } },
|
|
29
|
+
* required: ['name'],
|
|
30
|
+
* });
|
|
31
|
+
* const result = await model.run(prompt, { generationConfig: { grammar } });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function jsonSchemaToGbnf(schema) {
|
|
35
|
+
return NativeXybrid.jsonSchemaToGbnf(typeof schema === 'string' ? schema : JSON.stringify(schema));
|
|
36
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import NativeXybrid from './NativeXybrid';
|
|
2
|
+
import { bindCancel } from './cancel';
|
|
3
|
+
import { StreamingSession } from './session';
|
|
4
|
+
import { fromWireDownloadStatus, fromWireGenerationConfig, fromWireResult, fromWireStreamToken, toWireEnvelope, toWireRunOptions, } from './wire';
|
|
5
|
+
function toWireSource(source) {
|
|
6
|
+
switch (source.kind) {
|
|
7
|
+
case 'registry':
|
|
8
|
+
case 'registrySpeculative':
|
|
9
|
+
return { kind: source.kind, value: source.id };
|
|
10
|
+
case 'bundle':
|
|
11
|
+
case 'directory':
|
|
12
|
+
case 'modelFile':
|
|
13
|
+
return { kind: source.kind, value: source.path };
|
|
14
|
+
case 'huggingFace':
|
|
15
|
+
return {
|
|
16
|
+
kind: source.kind,
|
|
17
|
+
value: source.repo,
|
|
18
|
+
...(source.revision ? { revision: source.revision } : {}),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A cheap description of a model. Creating one does no I/O; {@link load}
|
|
24
|
+
* resolves, downloads if needed, and loads it.
|
|
25
|
+
*/
|
|
26
|
+
export class ModelLoader {
|
|
27
|
+
constructor(source) {
|
|
28
|
+
this.source = source;
|
|
29
|
+
}
|
|
30
|
+
/** Describe any source. */
|
|
31
|
+
static from(source) {
|
|
32
|
+
return new ModelLoader(source);
|
|
33
|
+
}
|
|
34
|
+
/** A model from the Xybrid registry. The recommended path. */
|
|
35
|
+
static fromRegistry(id) {
|
|
36
|
+
return new ModelLoader({ kind: 'registry', id });
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Serve from the cloud gateway while the registry weights download in the
|
|
40
|
+
* background, instead of blocking on the download. `load()` then resolves
|
|
41
|
+
* almost immediately with a model that switches to on-device by itself.
|
|
42
|
+
* Needs an API key and an uncached model — {@link willSpeculate} tells you
|
|
43
|
+
* up front — otherwise it behaves like {@link fromRegistry}. LLM/chat only.
|
|
44
|
+
*/
|
|
45
|
+
static fromRegistrySpeculative(id) {
|
|
46
|
+
return new ModelLoader({ kind: 'registrySpeculative', id });
|
|
47
|
+
}
|
|
48
|
+
/** A local `.xyb` bundle. */
|
|
49
|
+
static fromBundle(path) {
|
|
50
|
+
return new ModelLoader({ kind: 'bundle', path });
|
|
51
|
+
}
|
|
52
|
+
/** A local directory containing `model_metadata.json`. */
|
|
53
|
+
static fromDirectory(path) {
|
|
54
|
+
return new ModelLoader({ kind: 'directory', path });
|
|
55
|
+
}
|
|
56
|
+
/** A Hugging Face repository (`org/repo` or `org/repo:variant`), optionally pinned. */
|
|
57
|
+
static fromHuggingFace(repo, options = {}) {
|
|
58
|
+
return new ModelLoader({ kind: 'huggingFace', repo, ...options });
|
|
59
|
+
}
|
|
60
|
+
/** A raw GGUF file; `model_metadata.json` is generated next to it if absent. */
|
|
61
|
+
static fromModelFile(path) {
|
|
62
|
+
return new ModelLoader({ kind: 'modelFile', path });
|
|
63
|
+
}
|
|
64
|
+
/** Resolve, download if needed, and load. Rejects with a typed `xybrid_*` code. */
|
|
65
|
+
async load() {
|
|
66
|
+
return new Model(await NativeXybrid.loadModel(toWireSource(this.source)));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Whether {@link load} would actually speculate: a speculative source, an
|
|
70
|
+
* API key, and a model not yet cached. Never touches the network.
|
|
71
|
+
*/
|
|
72
|
+
async willSpeculate() {
|
|
73
|
+
if (this.source.kind !== 'registrySpeculative')
|
|
74
|
+
return false;
|
|
75
|
+
return NativeXybrid.willSpeculate(this.source.id);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Start downloading the weights in the background without loading them —
|
|
79
|
+
* the object to drive a progress bar from. The download fills the cache, so
|
|
80
|
+
* the {@link load} afterwards returns at once. Resolves `null` for sources
|
|
81
|
+
* with nothing to fetch (bundle, directory, Hugging Face, model file).
|
|
82
|
+
*/
|
|
83
|
+
async download(options = {}) {
|
|
84
|
+
const { source } = this;
|
|
85
|
+
if (source.kind !== 'registry' && source.kind !== 'registrySpeculative')
|
|
86
|
+
return null;
|
|
87
|
+
return new ModelDownload(await NativeXybrid.startDownload(source.id, options.platform ?? null));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** A model loaded in native memory. Call {@link release} when done with it. */
|
|
91
|
+
export class Model {
|
|
92
|
+
/** @internal Use {@link ModelLoader.load}. */
|
|
93
|
+
constructor(
|
|
94
|
+
/** Opaque native handle — not the model id; see {@link info}. */
|
|
95
|
+
handle) {
|
|
96
|
+
this.handle = handle;
|
|
97
|
+
}
|
|
98
|
+
/** Identity and capabilities, in one native call. */
|
|
99
|
+
async info() {
|
|
100
|
+
const raw = (await NativeXybrid.modelInfo(this.handle));
|
|
101
|
+
return {
|
|
102
|
+
...raw,
|
|
103
|
+
defaultGenerationConfig: fromWireGenerationConfig(raw.defaultGenerationConfig),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Whether the weights are resident (false after {@link unload} until the next run). */
|
|
107
|
+
isLoaded() {
|
|
108
|
+
return NativeXybrid.isLoaded(this.handle);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Run one inference. Pass sampling parameters under `generationConfig`,
|
|
112
|
+
* a conversation under `context`, and an `AbortSignal` under `signal`.
|
|
113
|
+
*/
|
|
114
|
+
async run(envelope, options = {}) {
|
|
115
|
+
const wireEnvelope = toWireEnvelope(envelope);
|
|
116
|
+
const cancel = await bindCancel(options.signal);
|
|
117
|
+
try {
|
|
118
|
+
const wire = await NativeXybrid.run(this.handle, wireEnvelope, toWireRunOptions(options, { context: options.context?.handle, cancel: cancel.token }));
|
|
119
|
+
return fromWireResult(wire);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
await cancel.release();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Stream inference token by token. Yields each {@link StreamToken}; the
|
|
127
|
+
* final {@link InferenceResult} is the generator's return value.
|
|
128
|
+
*
|
|
129
|
+
* ```ts
|
|
130
|
+
* for await (const token of model.runStreaming(Envelope.text('Hi'))) {
|
|
131
|
+
* setText((prev) => prev + token.token);
|
|
132
|
+
* }
|
|
133
|
+
* ```
|
|
134
|
+
*
|
|
135
|
+
* Generation stops natively when iteration ends — completion, `break`, a
|
|
136
|
+
* thrown error, or an aborted `signal`. A generator that is merely
|
|
137
|
+
* abandoned is never cleaned up (JS runs no `finally` on GC), so on
|
|
138
|
+
* unmount abort the signal or call `gen.return()`.
|
|
139
|
+
*
|
|
140
|
+
* When the turn ends on tool calls, the terminal token carries `toolCalls`
|
|
141
|
+
* and `rawText` (pass it to `Envelope.toolResults`).
|
|
142
|
+
*/
|
|
143
|
+
async *runStreaming(envelope, options = {}) {
|
|
144
|
+
const wireEnvelope = toWireEnvelope(envelope);
|
|
145
|
+
const cancel = await bindCancel(options.signal);
|
|
146
|
+
let stream = null;
|
|
147
|
+
try {
|
|
148
|
+
stream = await NativeXybrid.streamStart(this.handle, wireEnvelope, toWireRunOptions(options, { context: options.context?.handle, cancel: cancel.token }));
|
|
149
|
+
for (;;) {
|
|
150
|
+
const event = (await NativeXybrid.streamNext(stream));
|
|
151
|
+
// `null`: the stream was disposed underneath us (model released).
|
|
152
|
+
if (event == null)
|
|
153
|
+
return undefined;
|
|
154
|
+
switch (event.kind) {
|
|
155
|
+
case 'token':
|
|
156
|
+
yield fromWireStreamToken(event.token);
|
|
157
|
+
break;
|
|
158
|
+
case 'complete':
|
|
159
|
+
return fromWireResult(event.result);
|
|
160
|
+
default:
|
|
161
|
+
throw new Error(`Unexpected stream event: ${String(event.kind)}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
// Disposing aborts native generation; idempotent after completion.
|
|
167
|
+
if (stream !== null)
|
|
168
|
+
await NativeXybrid.dispose(stream).catch(() => { });
|
|
169
|
+
await cancel.release();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Open a live ASR session: feed microphone PCM in, read partial transcripts
|
|
174
|
+
* out. Audio must be Float32, mono, 16 kHz. Rejects with
|
|
175
|
+
* `xybrid_streaming_unsupported` for a model that cannot stream.
|
|
176
|
+
*/
|
|
177
|
+
async stream(config = {}) {
|
|
178
|
+
return new StreamingSession(await NativeXybrid.openStreamingSession(this.handle, config));
|
|
179
|
+
}
|
|
180
|
+
/** Prime the model so first-token latency measures inference, not cold start. */
|
|
181
|
+
warmup() {
|
|
182
|
+
return NativeXybrid.warmup(this.handle);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Free the weights while keeping this handle valid — the next run reloads
|
|
186
|
+
* them. Contrast with {@link release}, which frees the handle.
|
|
187
|
+
*/
|
|
188
|
+
unload() {
|
|
189
|
+
return NativeXybrid.unload(this.handle);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Whether runs are currently answered by the cloud because the local
|
|
193
|
+
* weights aren't ready (speculative loads). Predicts the next run;
|
|
194
|
+
* `InferenceResult.executionTarget` reports what a finished run did.
|
|
195
|
+
*/
|
|
196
|
+
isCloudServing() {
|
|
197
|
+
return NativeXybrid.isCloudServing(this.handle);
|
|
198
|
+
}
|
|
199
|
+
/** Download progress of a speculative load; `ready` at 1.0 for local models. */
|
|
200
|
+
async downloadStatus() {
|
|
201
|
+
return fromWireDownloadStatus((await NativeXybrid.downloadStatus(this.handle)));
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Yield the speculative download's status whenever it changes, ending
|
|
205
|
+
* after a terminal state. A local model yields one `ready` frame.
|
|
206
|
+
*/
|
|
207
|
+
downloadProgress(options = {}) {
|
|
208
|
+
return pollDownload(() => this.downloadStatus(), options.intervalMs);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Wait natively until the download settles or `timeoutMs` elapses, then
|
|
212
|
+
* report it. `0` makes it a non-blocking read.
|
|
213
|
+
*/
|
|
214
|
+
async awaitDownload(timeoutMs) {
|
|
215
|
+
return fromWireDownloadStatus((await NativeXybrid.awaitDownload(this.handle, timeoutMs)));
|
|
216
|
+
}
|
|
217
|
+
/** TTS voices; empty for models without voices. */
|
|
218
|
+
async voices() {
|
|
219
|
+
return (await NativeXybrid.voices(this.handle));
|
|
220
|
+
}
|
|
221
|
+
/** The default TTS voice, if the model has voices. */
|
|
222
|
+
async defaultVoice() {
|
|
223
|
+
return (await NativeXybrid.defaultVoice(this.handle));
|
|
224
|
+
}
|
|
225
|
+
/** One TTS voice by id. */
|
|
226
|
+
async voice(voiceId) {
|
|
227
|
+
return (await NativeXybrid.voice(this.handle, voiceId));
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Free the native model — weights live in the native heap, so release
|
|
231
|
+
* models you no longer need. Also stops token streams started from it.
|
|
232
|
+
* Later calls on this instance reject with `xybrid_handle`.
|
|
233
|
+
*/
|
|
234
|
+
release() {
|
|
235
|
+
return NativeXybrid.dispose(this.handle);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const TERMINAL_STATES = new Set(['ready', 'failed', 'cancelled']);
|
|
239
|
+
/**
|
|
240
|
+
* Poll `read` and yield each changed status, ending after a terminal state.
|
|
241
|
+
* The native side throttles its own updates (~10/s), so a 200 ms default
|
|
242
|
+
* loses nothing a progress bar can show.
|
|
243
|
+
*/
|
|
244
|
+
async function* pollDownload(read, intervalMs = 200) {
|
|
245
|
+
const delay = Math.max(16, intervalMs);
|
|
246
|
+
let previous = null;
|
|
247
|
+
for (;;) {
|
|
248
|
+
const status = await read();
|
|
249
|
+
if (previous === null ||
|
|
250
|
+
status.state !== previous.state ||
|
|
251
|
+
status.downloadedBytes !== previous.downloadedBytes ||
|
|
252
|
+
status.progress !== previous.progress) {
|
|
253
|
+
yield status;
|
|
254
|
+
previous = status;
|
|
255
|
+
}
|
|
256
|
+
if (TERMINAL_STATES.has(status.state))
|
|
257
|
+
return;
|
|
258
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* A model download running in the background, separate from loading. Start
|
|
263
|
+
* one with {@link ModelLoader.download}. Releasing the handle does not stop
|
|
264
|
+
* the transfer — call {@link cancel}.
|
|
265
|
+
*/
|
|
266
|
+
export class ModelDownload {
|
|
267
|
+
/** @internal */
|
|
268
|
+
constructor(handle) {
|
|
269
|
+
this.handle = handle;
|
|
270
|
+
}
|
|
271
|
+
/** Progress, bytes and state in one consistent read. */
|
|
272
|
+
async status() {
|
|
273
|
+
return fromWireDownloadStatus((await NativeXybrid.downloadHandleStatus(this.handle)));
|
|
274
|
+
}
|
|
275
|
+
/** The failure message once the download failed, else `null`. */
|
|
276
|
+
error() {
|
|
277
|
+
return NativeXybrid.downloadHandleError(this.handle);
|
|
278
|
+
}
|
|
279
|
+
/** Stop the transfer. The status then settles on `cancelled`. */
|
|
280
|
+
cancel() {
|
|
281
|
+
return NativeXybrid.cancelDownload(this.handle);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Yield the status whenever it changes, ending after a terminal state
|
|
285
|
+
* (`ready`, `failed`, `cancelled`). Polls natively every `intervalMs`.
|
|
286
|
+
*
|
|
287
|
+
* ```ts
|
|
288
|
+
* for await (const s of download.progress()) setProgress(s.progress);
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
progress(options = {}) {
|
|
292
|
+
return pollDownload(() => this.status(), options.intervalMs);
|
|
293
|
+
}
|
|
294
|
+
/** Free the handle (the transfer keeps going unless cancelled). */
|
|
295
|
+
release() {
|
|
296
|
+
return NativeXybrid.dispose(this.handle);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import NativeXybrid from './NativeXybrid';
|
|
2
|
+
import { fromWirePipelineResult, toWireEnvelope, } from './wire';
|
|
3
|
+
/**
|
|
4
|
+
* A multi-stage pipeline (e.g. ASR → LLM → TTS) defined in YAML. A run
|
|
5
|
+
* returns every stage's output, so a voice assistant can show what it heard
|
|
6
|
+
* and what it answered while it plays the audio:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* const pipeline = await Pipeline.fromFile(path);
|
|
10
|
+
* const result = await pipeline.run(Envelope.audio(pcmBase64));
|
|
11
|
+
* const heard = result.stages.find((s) => s.stageId === 'asr')?.text;
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export class Pipeline {
|
|
15
|
+
constructor(
|
|
16
|
+
/** Opaque native handle. */
|
|
17
|
+
handle) {
|
|
18
|
+
this.handle = handle;
|
|
19
|
+
}
|
|
20
|
+
/** Parse and load a pipeline from YAML content. */
|
|
21
|
+
static async fromYaml(yaml) {
|
|
22
|
+
return new Pipeline(await NativeXybrid.loadPipeline({ kind: 'yaml', value: yaml }));
|
|
23
|
+
}
|
|
24
|
+
/** Read, parse and load a pipeline YAML file. */
|
|
25
|
+
static async fromFile(path) {
|
|
26
|
+
return new Pipeline(await NativeXybrid.loadPipeline({ kind: 'file', value: path }));
|
|
27
|
+
}
|
|
28
|
+
/** Load a pipeline bundle. */
|
|
29
|
+
static async fromBundle(path) {
|
|
30
|
+
return new Pipeline(await NativeXybrid.loadPipeline({ kind: 'bundle', value: path }));
|
|
31
|
+
}
|
|
32
|
+
/** Name and stage ids, in execution order. */
|
|
33
|
+
async info() {
|
|
34
|
+
return (await NativeXybrid.pipelineInfo(this.handle));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Run every stage, downloading any missing model first. Per-stage
|
|
38
|
+
* generation settings belong in the YAML; only `correlationId` applies here.
|
|
39
|
+
*/
|
|
40
|
+
async run(envelope, options = {}) {
|
|
41
|
+
const wire = await NativeXybrid.runPipeline(this.handle, toWireEnvelope(envelope), options.correlationId !== undefined ? { correlationId: options.correlationId } : null);
|
|
42
|
+
return fromWirePipelineResult(wire);
|
|
43
|
+
}
|
|
44
|
+
/** Free the native pipeline. */
|
|
45
|
+
release() {
|
|
46
|
+
return NativeXybrid.dispose(this.handle);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Generation-config presets with the same values as the Swift and Kotlin
|
|
2
|
+
// `GenerationConfigs`. Pure TS — no native call.
|
|
3
|
+
/**
|
|
4
|
+
* Greedy decoding (deterministic, temperature 0): the usual choice for
|
|
5
|
+
* extraction and tool calling.
|
|
6
|
+
*/
|
|
7
|
+
export function greedy(options = {}) {
|
|
8
|
+
return {
|
|
9
|
+
temperature: 0.0,
|
|
10
|
+
topP: 1.0,
|
|
11
|
+
topK: 0,
|
|
12
|
+
stopSequences: [],
|
|
13
|
+
...options,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Higher temperature, for more varied output. */
|
|
17
|
+
export function creative(options = {}) {
|
|
18
|
+
return {
|
|
19
|
+
temperature: 0.9,
|
|
20
|
+
topP: 0.95,
|
|
21
|
+
topK: 50,
|
|
22
|
+
stopSequences: [],
|
|
23
|
+
...options,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Preset factories for {@link GenerationConfig}. */
|
|
27
|
+
export const GenerationConfigs = { greedy, creative };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import NativeXybrid from './NativeXybrid';
|
|
2
|
+
import { float32ToBase64 } from './base64';
|
|
3
|
+
/**
|
|
4
|
+
* A live ASR session: feed microphone PCM in, read partial transcripts out.
|
|
5
|
+
* Open one with `model.stream()`.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* const session = await model.stream();
|
|
9
|
+
* (async () => {
|
|
10
|
+
* for await (const partial of session.partials()) setCaption(partial.text);
|
|
11
|
+
* })();
|
|
12
|
+
* // from your audio callback (Float32, mono, 16 kHz):
|
|
13
|
+
* await session.feed(samples);
|
|
14
|
+
* // when the user stops talking:
|
|
15
|
+
* const transcript = await session.flush();
|
|
16
|
+
* await session.release();
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export class StreamingSession {
|
|
20
|
+
/** @internal Use `model.stream()`. */
|
|
21
|
+
constructor(
|
|
22
|
+
/** Opaque native handle. */
|
|
23
|
+
handle) {
|
|
24
|
+
this.handle = handle;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Queue PCM samples (Float32, mono, 16 kHz). Transcription happens off the
|
|
28
|
+
* JS thread; this only waits when the native queue is full.
|
|
29
|
+
*/
|
|
30
|
+
feed(samples) {
|
|
31
|
+
return NativeXybrid.sessionFeed(this.handle, float32ToBase64(samples));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Partial transcripts as they arrive, ending when the session finishes
|
|
35
|
+
* (after {@link flush} or {@link cancel}). Each partial is cumulative:
|
|
36
|
+
* render it in place of the previous one. Use one iterator per session.
|
|
37
|
+
*/
|
|
38
|
+
async *partials() {
|
|
39
|
+
for (;;) {
|
|
40
|
+
const partial = (await NativeXybrid.sessionNextPartial(this.handle));
|
|
41
|
+
if (partial == null)
|
|
42
|
+
return;
|
|
43
|
+
yield partial;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Drain the buffered audio and return the complete transcript. The session
|
|
48
|
+
* is over afterwards.
|
|
49
|
+
*/
|
|
50
|
+
flush() {
|
|
51
|
+
return NativeXybrid.sessionFlush(this.handle);
|
|
52
|
+
}
|
|
53
|
+
/** Start over on fresh audio without reloading the model. */
|
|
54
|
+
reset() {
|
|
55
|
+
return NativeXybrid.sessionReset(this.handle);
|
|
56
|
+
}
|
|
57
|
+
/** Stop and discard buffered audio — the "user walked away" path. */
|
|
58
|
+
cancel() {
|
|
59
|
+
return NativeXybrid.sessionCancel(this.handle);
|
|
60
|
+
}
|
|
61
|
+
/** Whether the session still accepts audio. */
|
|
62
|
+
isRunning() {
|
|
63
|
+
return NativeXybrid.sessionIsRunning(this.handle);
|
|
64
|
+
}
|
|
65
|
+
/** Free the native session (cancelling it if still running). */
|
|
66
|
+
release() {
|
|
67
|
+
return NativeXybrid.dispose(this.handle);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Public types. They mirror the bolt `#[data]` records in
|
|
2
|
+
// crates/xybrid-bolt/src/lib.rs field for field (camelCase), so a value means
|
|
3
|
+
// the same thing here as in the Swift and Kotlin SDKs. tests/parity.test.mjs
|
|
4
|
+
// fails when bolt gains a field or export that is neither mapped here nor
|
|
5
|
+
// listed as intentionally excluded in parity.json.
|
|
6
|
+
//
|
|
7
|
+
// Binary payloads (audio, images) cross as base64 strings: TurboModules have
|
|
8
|
+
// no ArrayBuffer type. See README "Binary payloads".
|
|
9
|
+
export {};
|