@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.
Files changed (87) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +298 -0
  3. package/android/build.gradle +61 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/java/ai/xybrid/reactnative/XybridCodec.kt +481 -0
  6. package/android/src/main/java/ai/xybrid/reactnative/XybridHandles.kt +139 -0
  7. package/android/src/main/java/ai/xybrid/reactnative/XybridModule.kt +665 -0
  8. package/android/src/main/java/ai/xybrid/reactnative/XybridPackage.kt +27 -0
  9. package/ios/XybridCodec.swift +508 -0
  10. package/ios/XybridHandles.swift +191 -0
  11. package/ios/XybridModule.mm +494 -0
  12. package/ios/XybridModuleImpl.swift +887 -0
  13. package/ios/XybridSwift/Xybrid.swift +1401 -0
  14. package/ios/XybridSwift/xybrid_bolt.swift +3198 -0
  15. package/ios/xybrid_natives.rb +178 -0
  16. package/lib/commonjs/NativeXybrid.js +24 -0
  17. package/lib/commonjs/base64.js +45 -0
  18. package/lib/commonjs/cancel.js +35 -0
  19. package/lib/commonjs/context.js +67 -0
  20. package/lib/commonjs/envelope.js +79 -0
  21. package/lib/commonjs/errors.js +61 -0
  22. package/lib/commonjs/index.js +59 -0
  23. package/lib/commonjs/model.js +307 -0
  24. package/lib/commonjs/pipeline.js +55 -0
  25. package/lib/commonjs/presets.js +32 -0
  26. package/lib/commonjs/session.js +76 -0
  27. package/lib/commonjs/types.js +10 -0
  28. package/lib/commonjs/wire.js +285 -0
  29. package/lib/commonjs/xybrid.js +133 -0
  30. package/lib/module/NativeXybrid.js +22 -0
  31. package/lib/module/base64.js +41 -0
  32. package/lib/module/cancel.js +29 -0
  33. package/lib/module/context.js +60 -0
  34. package/lib/module/envelope.js +73 -0
  35. package/lib/module/errors.js +56 -0
  36. package/lib/module/index.js +36 -0
  37. package/lib/module/model.js +298 -0
  38. package/lib/module/pipeline.js +48 -0
  39. package/lib/module/presets.js +27 -0
  40. package/lib/module/session.js +69 -0
  41. package/lib/module/types.js +9 -0
  42. package/lib/module/wire.js +271 -0
  43. package/lib/module/xybrid.js +127 -0
  44. package/lib/typescript/NativeXybrid.d.ts +68 -0
  45. package/lib/typescript/NativeXybrid.d.ts.map +1 -0
  46. package/lib/typescript/base64.d.ts +5 -0
  47. package/lib/typescript/base64.d.ts.map +1 -0
  48. package/lib/typescript/cancel.d.ts +24 -0
  49. package/lib/typescript/cancel.d.ts.map +1 -0
  50. package/lib/typescript/context.d.ts +40 -0
  51. package/lib/typescript/context.d.ts.map +1 -0
  52. package/lib/typescript/envelope.d.ts +51 -0
  53. package/lib/typescript/envelope.d.ts.map +1 -0
  54. package/lib/typescript/errors.d.ts +12 -0
  55. package/lib/typescript/errors.d.ts.map +1 -0
  56. package/lib/typescript/index.d.ts +28 -0
  57. package/lib/typescript/index.d.ts.map +1 -0
  58. package/lib/typescript/model.d.ts +211 -0
  59. package/lib/typescript/model.d.ts.map +1 -0
  60. package/lib/typescript/pipeline.d.ts +35 -0
  61. package/lib/typescript/pipeline.d.ts.map +1 -0
  62. package/lib/typescript/presets.d.ts +21 -0
  63. package/lib/typescript/presets.d.ts.map +1 -0
  64. package/lib/typescript/session.d.ts +50 -0
  65. package/lib/typescript/session.d.ts.map +1 -0
  66. package/lib/typescript/types.d.ts +313 -0
  67. package/lib/typescript/types.d.ts.map +1 -0
  68. package/lib/typescript/wire.d.ts +130 -0
  69. package/lib/typescript/wire.d.ts.map +1 -0
  70. package/lib/typescript/xybrid.d.ts +89 -0
  71. package/lib/typescript/xybrid.d.ts.map +1 -0
  72. package/package.json +88 -0
  73. package/react-native-xybrid.podspec +50 -0
  74. package/src/NativeXybrid.ts +131 -0
  75. package/src/base64.ts +43 -0
  76. package/src/cancel.ts +47 -0
  77. package/src/context.ts +70 -0
  78. package/src/envelope.ts +115 -0
  79. package/src/errors.ts +67 -0
  80. package/src/index.ts +91 -0
  81. package/src/model.ts +408 -0
  82. package/src/pipeline.ts +66 -0
  83. package/src/presets.ts +36 -0
  84. package/src/session.ts +76 -0
  85. package/src/types.ts +362 -0
  86. package/src/wire.ts +408 -0
  87. package/src/xybrid.ts +176 -0
package/src/model.ts ADDED
@@ -0,0 +1,408 @@
1
+ import NativeXybrid from './NativeXybrid';
2
+ import { bindCancel, type AbortSignalLike } from './cancel';
3
+ import type { ConversationContext } from './context';
4
+ import { StreamingSession } from './session';
5
+ import type {
6
+ AbortSignalKind,
7
+ DownloadStatus,
8
+ Envelope,
9
+ GenerationConfig,
10
+ InferenceResult,
11
+ ModelInfo,
12
+ StreamToken,
13
+ StreamingConfig,
14
+ VoiceInfo,
15
+ } from './types';
16
+ import {
17
+ fromWireDownloadStatus,
18
+ fromWireGenerationConfig,
19
+ fromWireResult,
20
+ fromWireStreamToken,
21
+ toWireEnvelope,
22
+ toWireRunOptions,
23
+ type WireGenerationConfig,
24
+ type WireStreamEvent,
25
+ } from './wire';
26
+
27
+ /**
28
+ * Per-call options, mirroring bolt's `XybridRunOptions` plus the two handles
29
+ * the other SDKs pass as separate arguments (a conversation context and a
30
+ * stop button).
31
+ */
32
+ export interface RunOptions {
33
+ /** Sampling parameters for LLM inference. */
34
+ generationConfig?: GenerationConfig;
35
+ /** Device-stress signals that abort the run early. */
36
+ abortOn?: AbortSignalKind[];
37
+ /** Allow this call to fall back to the cloud gateway under device stress. */
38
+ fallbackToCloud?: boolean;
39
+ /** Tokens to emit after an abort signal before stopping. */
40
+ maxGraceTokens?: number;
41
+ /** Correlation id threaded into telemetry for this call. */
42
+ correlationId?: string;
43
+ /**
44
+ * Seed the run with this conversation's history (multi-turn chat). The
45
+ * context is not modified: push the turns you want to keep yourself.
46
+ */
47
+ context?: ConversationContext;
48
+ /**
49
+ * Stop button. Aborting stops generation at the next token boundary. A
50
+ * batch `run` is only cancellable before generation starts; use
51
+ * `runStreaming` when a mid-flight stop matters.
52
+ */
53
+ signal?: AbortSignalLike;
54
+ }
55
+
56
+ /** Where a model comes from. Build one with the {@link ModelLoader} factories. */
57
+ export type ModelSource =
58
+ | { kind: 'registry'; id: string }
59
+ | { kind: 'registrySpeculative'; id: string }
60
+ | { kind: 'bundle'; path: string }
61
+ | { kind: 'directory'; path: string }
62
+ | { kind: 'huggingFace'; repo: string; revision?: string }
63
+ | { kind: 'modelFile'; path: string };
64
+
65
+ function toWireSource(source: ModelSource): { kind: string; value: string; revision?: string } {
66
+ switch (source.kind) {
67
+ case 'registry':
68
+ case 'registrySpeculative':
69
+ return { kind: source.kind, value: source.id };
70
+ case 'bundle':
71
+ case 'directory':
72
+ case 'modelFile':
73
+ return { kind: source.kind, value: source.path };
74
+ case 'huggingFace':
75
+ return {
76
+ kind: source.kind,
77
+ value: source.repo,
78
+ ...(source.revision ? { revision: source.revision } : {}),
79
+ };
80
+ }
81
+ }
82
+
83
+ /**
84
+ * A cheap description of a model. Creating one does no I/O; {@link load}
85
+ * resolves, downloads if needed, and loads it.
86
+ */
87
+ export class ModelLoader {
88
+ private constructor(readonly source: ModelSource) {}
89
+
90
+ /** Describe any source. */
91
+ static from(source: ModelSource): ModelLoader {
92
+ return new ModelLoader(source);
93
+ }
94
+
95
+ /** A model from the Xybrid registry. The recommended path. */
96
+ static fromRegistry(id: string): ModelLoader {
97
+ return new ModelLoader({ kind: 'registry', id });
98
+ }
99
+
100
+ /**
101
+ * Serve from the cloud gateway while the registry weights download in the
102
+ * background, instead of blocking on the download. `load()` then resolves
103
+ * almost immediately with a model that switches to on-device by itself.
104
+ * Needs an API key and an uncached model — {@link willSpeculate} tells you
105
+ * up front — otherwise it behaves like {@link fromRegistry}. LLM/chat only.
106
+ */
107
+ static fromRegistrySpeculative(id: string): ModelLoader {
108
+ return new ModelLoader({ kind: 'registrySpeculative', id });
109
+ }
110
+
111
+ /** A local `.xyb` bundle. */
112
+ static fromBundle(path: string): ModelLoader {
113
+ return new ModelLoader({ kind: 'bundle', path });
114
+ }
115
+
116
+ /** A local directory containing `model_metadata.json`. */
117
+ static fromDirectory(path: string): ModelLoader {
118
+ return new ModelLoader({ kind: 'directory', path });
119
+ }
120
+
121
+ /** A Hugging Face repository (`org/repo` or `org/repo:variant`), optionally pinned. */
122
+ static fromHuggingFace(repo: string, options: { revision?: string } = {}): ModelLoader {
123
+ return new ModelLoader({ kind: 'huggingFace', repo, ...options });
124
+ }
125
+
126
+ /** A raw GGUF file; `model_metadata.json` is generated next to it if absent. */
127
+ static fromModelFile(path: string): ModelLoader {
128
+ return new ModelLoader({ kind: 'modelFile', path });
129
+ }
130
+
131
+ /** Resolve, download if needed, and load. Rejects with a typed `xybrid_*` code. */
132
+ async load(): Promise<Model> {
133
+ return new Model(await NativeXybrid.loadModel(toWireSource(this.source)));
134
+ }
135
+
136
+ /**
137
+ * Whether {@link load} would actually speculate: a speculative source, an
138
+ * API key, and a model not yet cached. Never touches the network.
139
+ */
140
+ async willSpeculate(): Promise<boolean> {
141
+ if (this.source.kind !== 'registrySpeculative') return false;
142
+ return NativeXybrid.willSpeculate(this.source.id);
143
+ }
144
+
145
+ /**
146
+ * Start downloading the weights in the background without loading them —
147
+ * the object to drive a progress bar from. The download fills the cache, so
148
+ * the {@link load} afterwards returns at once. Resolves `null` for sources
149
+ * with nothing to fetch (bundle, directory, Hugging Face, model file).
150
+ */
151
+ async download(options: { platform?: string } = {}): Promise<ModelDownload | null> {
152
+ const { source } = this;
153
+ if (source.kind !== 'registry' && source.kind !== 'registrySpeculative') return null;
154
+ return new ModelDownload(await NativeXybrid.startDownload(source.id, options.platform ?? null));
155
+ }
156
+ }
157
+
158
+ /** A model loaded in native memory. Call {@link release} when done with it. */
159
+ export class Model {
160
+ /** @internal Use {@link ModelLoader.load}. */
161
+ constructor(
162
+ /** Opaque native handle — not the model id; see {@link info}. */
163
+ readonly handle: string,
164
+ ) {}
165
+
166
+ /** Identity and capabilities, in one native call. */
167
+ async info(): Promise<ModelInfo> {
168
+ const raw = (await NativeXybrid.modelInfo(this.handle)) as Omit<
169
+ ModelInfo,
170
+ 'defaultGenerationConfig'
171
+ > & { defaultGenerationConfig: WireGenerationConfig };
172
+ return {
173
+ ...raw,
174
+ defaultGenerationConfig: fromWireGenerationConfig(raw.defaultGenerationConfig),
175
+ };
176
+ }
177
+
178
+ /** Whether the weights are resident (false after {@link unload} until the next run). */
179
+ isLoaded(): Promise<boolean> {
180
+ return NativeXybrid.isLoaded(this.handle);
181
+ }
182
+
183
+ /**
184
+ * Run one inference. Pass sampling parameters under `generationConfig`,
185
+ * a conversation under `context`, and an `AbortSignal` under `signal`.
186
+ */
187
+ async run(envelope: Envelope, options: RunOptions = {}): Promise<InferenceResult> {
188
+ const wireEnvelope = toWireEnvelope(envelope);
189
+ const cancel = await bindCancel(options.signal);
190
+ try {
191
+ const wire = await NativeXybrid.run(
192
+ this.handle,
193
+ wireEnvelope,
194
+ toWireRunOptions(options, { context: options.context?.handle, cancel: cancel.token }),
195
+ );
196
+ return fromWireResult(wire as Parameters<typeof fromWireResult>[0]);
197
+ } finally {
198
+ await cancel.release();
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Stream inference token by token. Yields each {@link StreamToken}; the
204
+ * final {@link InferenceResult} is the generator's return value.
205
+ *
206
+ * ```ts
207
+ * for await (const token of model.runStreaming(Envelope.text('Hi'))) {
208
+ * setText((prev) => prev + token.token);
209
+ * }
210
+ * ```
211
+ *
212
+ * Generation stops natively when iteration ends — completion, `break`, a
213
+ * thrown error, or an aborted `signal`. A generator that is merely
214
+ * abandoned is never cleaned up (JS runs no `finally` on GC), so on
215
+ * unmount abort the signal or call `gen.return()`.
216
+ *
217
+ * When the turn ends on tool calls, the terminal token carries `toolCalls`
218
+ * and `rawText` (pass it to `Envelope.toolResults`).
219
+ */
220
+ async *runStreaming(
221
+ envelope: Envelope,
222
+ options: RunOptions = {},
223
+ ): AsyncGenerator<StreamToken, InferenceResult | undefined, void> {
224
+ const wireEnvelope = toWireEnvelope(envelope);
225
+ const cancel = await bindCancel(options.signal);
226
+ let stream: string | null = null;
227
+ try {
228
+ stream = await NativeXybrid.streamStart(
229
+ this.handle,
230
+ wireEnvelope,
231
+ toWireRunOptions(options, { context: options.context?.handle, cancel: cancel.token }),
232
+ );
233
+ for (;;) {
234
+ const event = (await NativeXybrid.streamNext(stream)) as WireStreamEvent | null;
235
+ // `null`: the stream was disposed underneath us (model released).
236
+ if (event == null) return undefined;
237
+ switch (event.kind) {
238
+ case 'token':
239
+ yield fromWireStreamToken(event.token);
240
+ break;
241
+ case 'complete':
242
+ return fromWireResult(event.result);
243
+ default:
244
+ throw new Error(
245
+ `Unexpected stream event: ${String((event as { kind?: unknown }).kind)}`,
246
+ );
247
+ }
248
+ }
249
+ } finally {
250
+ // Disposing aborts native generation; idempotent after completion.
251
+ if (stream !== null) await NativeXybrid.dispose(stream).catch(() => {});
252
+ await cancel.release();
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Open a live ASR session: feed microphone PCM in, read partial transcripts
258
+ * out. Audio must be Float32, mono, 16 kHz. Rejects with
259
+ * `xybrid_streaming_unsupported` for a model that cannot stream.
260
+ */
261
+ async stream(config: StreamingConfig = {}): Promise<StreamingSession> {
262
+ return new StreamingSession(await NativeXybrid.openStreamingSession(this.handle, config));
263
+ }
264
+
265
+ /** Prime the model so first-token latency measures inference, not cold start. */
266
+ warmup(): Promise<void> {
267
+ return NativeXybrid.warmup(this.handle);
268
+ }
269
+
270
+ /**
271
+ * Free the weights while keeping this handle valid — the next run reloads
272
+ * them. Contrast with {@link release}, which frees the handle.
273
+ */
274
+ unload(): Promise<void> {
275
+ return NativeXybrid.unload(this.handle);
276
+ }
277
+
278
+ /**
279
+ * Whether runs are currently answered by the cloud because the local
280
+ * weights aren't ready (speculative loads). Predicts the next run;
281
+ * `InferenceResult.executionTarget` reports what a finished run did.
282
+ */
283
+ isCloudServing(): Promise<boolean> {
284
+ return NativeXybrid.isCloudServing(this.handle);
285
+ }
286
+
287
+ /** Download progress of a speculative load; `ready` at 1.0 for local models. */
288
+ async downloadStatus(): Promise<DownloadStatus> {
289
+ return fromWireDownloadStatus(
290
+ (await NativeXybrid.downloadStatus(this.handle)) as DownloadStatus,
291
+ );
292
+ }
293
+
294
+ /**
295
+ * Yield the speculative download's status whenever it changes, ending
296
+ * after a terminal state. A local model yields one `ready` frame.
297
+ */
298
+ downloadProgress(options: { intervalMs?: number } = {}): AsyncGenerator<DownloadStatus, void, void> {
299
+ return pollDownload(() => this.downloadStatus(), options.intervalMs);
300
+ }
301
+
302
+ /**
303
+ * Wait natively until the download settles or `timeoutMs` elapses, then
304
+ * report it. `0` makes it a non-blocking read.
305
+ */
306
+ async awaitDownload(timeoutMs: number): Promise<DownloadStatus> {
307
+ return fromWireDownloadStatus(
308
+ (await NativeXybrid.awaitDownload(this.handle, timeoutMs)) as DownloadStatus,
309
+ );
310
+ }
311
+
312
+ /** TTS voices; empty for models without voices. */
313
+ async voices(): Promise<VoiceInfo[]> {
314
+ return (await NativeXybrid.voices(this.handle)) as VoiceInfo[];
315
+ }
316
+
317
+ /** The default TTS voice, if the model has voices. */
318
+ async defaultVoice(): Promise<VoiceInfo | null> {
319
+ return (await NativeXybrid.defaultVoice(this.handle)) as VoiceInfo | null;
320
+ }
321
+
322
+ /** One TTS voice by id. */
323
+ async voice(voiceId: string): Promise<VoiceInfo | null> {
324
+ return (await NativeXybrid.voice(this.handle, voiceId)) as VoiceInfo | null;
325
+ }
326
+
327
+ /**
328
+ * Free the native model — weights live in the native heap, so release
329
+ * models you no longer need. Also stops token streams started from it.
330
+ * Later calls on this instance reject with `xybrid_handle`.
331
+ */
332
+ release(): Promise<void> {
333
+ return NativeXybrid.dispose(this.handle);
334
+ }
335
+ }
336
+
337
+ const TERMINAL_STATES = new Set<DownloadStatus['state']>(['ready', 'failed', 'cancelled']);
338
+
339
+ /**
340
+ * Poll `read` and yield each changed status, ending after a terminal state.
341
+ * The native side throttles its own updates (~10/s), so a 200 ms default
342
+ * loses nothing a progress bar can show.
343
+ */
344
+ async function* pollDownload(
345
+ read: () => Promise<DownloadStatus>,
346
+ intervalMs = 200,
347
+ ): AsyncGenerator<DownloadStatus, void, void> {
348
+ const delay = Math.max(16, intervalMs);
349
+ let previous: DownloadStatus | null = null;
350
+ for (;;) {
351
+ const status = await read();
352
+ if (
353
+ previous === null ||
354
+ status.state !== previous.state ||
355
+ status.downloadedBytes !== previous.downloadedBytes ||
356
+ status.progress !== previous.progress
357
+ ) {
358
+ yield status;
359
+ previous = status;
360
+ }
361
+ if (TERMINAL_STATES.has(status.state)) return;
362
+ await new Promise((resolve) => setTimeout(resolve, delay));
363
+ }
364
+ }
365
+
366
+ /**
367
+ * A model download running in the background, separate from loading. Start
368
+ * one with {@link ModelLoader.download}. Releasing the handle does not stop
369
+ * the transfer — call {@link cancel}.
370
+ */
371
+ export class ModelDownload {
372
+ /** @internal */
373
+ constructor(readonly handle: string) {}
374
+
375
+ /** Progress, bytes and state in one consistent read. */
376
+ async status(): Promise<DownloadStatus> {
377
+ return fromWireDownloadStatus(
378
+ (await NativeXybrid.downloadHandleStatus(this.handle)) as DownloadStatus,
379
+ );
380
+ }
381
+
382
+ /** The failure message once the download failed, else `null`. */
383
+ error(): Promise<string | null> {
384
+ return NativeXybrid.downloadHandleError(this.handle);
385
+ }
386
+
387
+ /** Stop the transfer. The status then settles on `cancelled`. */
388
+ cancel(): Promise<void> {
389
+ return NativeXybrid.cancelDownload(this.handle);
390
+ }
391
+
392
+ /**
393
+ * Yield the status whenever it changes, ending after a terminal state
394
+ * (`ready`, `failed`, `cancelled`). Polls natively every `intervalMs`.
395
+ *
396
+ * ```ts
397
+ * for await (const s of download.progress()) setProgress(s.progress);
398
+ * ```
399
+ */
400
+ progress(options: { intervalMs?: number } = {}): AsyncGenerator<DownloadStatus, void, void> {
401
+ return pollDownload(() => this.status(), options.intervalMs);
402
+ }
403
+
404
+ /** Free the handle (the transfer keeps going unless cancelled). */
405
+ release(): Promise<void> {
406
+ return NativeXybrid.dispose(this.handle);
407
+ }
408
+ }
@@ -0,0 +1,66 @@
1
+ import NativeXybrid from './NativeXybrid';
2
+ import type { Envelope, PipelineInfo, PipelineResult } from './types';
3
+ import {
4
+ fromWirePipelineResult,
5
+ toWireEnvelope,
6
+ type WirePipelineResult,
7
+ } from './wire';
8
+
9
+ /**
10
+ * A multi-stage pipeline (e.g. ASR → LLM → TTS) defined in YAML. A run
11
+ * returns every stage's output, so a voice assistant can show what it heard
12
+ * and what it answered while it plays the audio:
13
+ *
14
+ * ```ts
15
+ * const pipeline = await Pipeline.fromFile(path);
16
+ * const result = await pipeline.run(Envelope.audio(pcmBase64));
17
+ * const heard = result.stages.find((s) => s.stageId === 'asr')?.text;
18
+ * ```
19
+ */
20
+ export class Pipeline {
21
+ private constructor(
22
+ /** Opaque native handle. */
23
+ readonly handle: string,
24
+ ) {}
25
+
26
+ /** Parse and load a pipeline from YAML content. */
27
+ static async fromYaml(yaml: string): Promise<Pipeline> {
28
+ return new Pipeline(await NativeXybrid.loadPipeline({ kind: 'yaml', value: yaml }));
29
+ }
30
+
31
+ /** Read, parse and load a pipeline YAML file. */
32
+ static async fromFile(path: string): Promise<Pipeline> {
33
+ return new Pipeline(await NativeXybrid.loadPipeline({ kind: 'file', value: path }));
34
+ }
35
+
36
+ /** Load a pipeline bundle. */
37
+ static async fromBundle(path: string): Promise<Pipeline> {
38
+ return new Pipeline(await NativeXybrid.loadPipeline({ kind: 'bundle', value: path }));
39
+ }
40
+
41
+ /** Name and stage ids, in execution order. */
42
+ async info(): Promise<PipelineInfo> {
43
+ return (await NativeXybrid.pipelineInfo(this.handle)) as PipelineInfo;
44
+ }
45
+
46
+ /**
47
+ * Run every stage, downloading any missing model first. Per-stage
48
+ * generation settings belong in the YAML; only `correlationId` applies here.
49
+ */
50
+ async run(
51
+ envelope: Envelope,
52
+ options: { correlationId?: string } = {},
53
+ ): Promise<PipelineResult> {
54
+ const wire = await NativeXybrid.runPipeline(
55
+ this.handle,
56
+ toWireEnvelope(envelope),
57
+ options.correlationId !== undefined ? { correlationId: options.correlationId } : null,
58
+ );
59
+ return fromWirePipelineResult(wire as WirePipelineResult);
60
+ }
61
+
62
+ /** Free the native pipeline. */
63
+ release(): Promise<void> {
64
+ return NativeXybrid.dispose(this.handle);
65
+ }
66
+ }
package/src/presets.ts ADDED
@@ -0,0 +1,36 @@
1
+ import type { GenerationConfig, ToolDefinition } from './types';
2
+
3
+ // Generation-config presets with the same values as the Swift and Kotlin
4
+ // `GenerationConfigs`. Pure TS — no native call.
5
+
6
+ /**
7
+ * Greedy decoding (deterministic, temperature 0): the usual choice for
8
+ * extraction and tool calling.
9
+ */
10
+ export function greedy(
11
+ options: { maxTokens?: number; grammar?: string; tools?: ToolDefinition[] } = {},
12
+ ): GenerationConfig {
13
+ return {
14
+ temperature: 0.0,
15
+ topP: 1.0,
16
+ topK: 0,
17
+ stopSequences: [],
18
+ ...options,
19
+ };
20
+ }
21
+
22
+ /** Higher temperature, for more varied output. */
23
+ export function creative(
24
+ options: { maxTokens?: number; tools?: ToolDefinition[] } = {},
25
+ ): GenerationConfig {
26
+ return {
27
+ temperature: 0.9,
28
+ topP: 0.95,
29
+ topK: 50,
30
+ stopSequences: [],
31
+ ...options,
32
+ };
33
+ }
34
+
35
+ /** Preset factories for {@link GenerationConfig}. */
36
+ export const GenerationConfigs = { greedy, creative };
package/src/session.ts ADDED
@@ -0,0 +1,76 @@
1
+ import NativeXybrid from './NativeXybrid';
2
+ import { float32ToBase64 } from './base64';
3
+ import type { PartialResult } from './types';
4
+
5
+ /**
6
+ * A live ASR session: feed microphone PCM in, read partial transcripts out.
7
+ * Open one with `model.stream()`.
8
+ *
9
+ * ```ts
10
+ * const session = await model.stream();
11
+ * (async () => {
12
+ * for await (const partial of session.partials()) setCaption(partial.text);
13
+ * })();
14
+ * // from your audio callback (Float32, mono, 16 kHz):
15
+ * await session.feed(samples);
16
+ * // when the user stops talking:
17
+ * const transcript = await session.flush();
18
+ * await session.release();
19
+ * ```
20
+ */
21
+ export class StreamingSession {
22
+ /** @internal Use `model.stream()`. */
23
+ constructor(
24
+ /** Opaque native handle. */
25
+ readonly handle: string,
26
+ ) {}
27
+
28
+ /**
29
+ * Queue PCM samples (Float32, mono, 16 kHz). Transcription happens off the
30
+ * JS thread; this only waits when the native queue is full.
31
+ */
32
+ feed(samples: Float32Array | ArrayLike<number>): Promise<void> {
33
+ return NativeXybrid.sessionFeed(this.handle, float32ToBase64(samples));
34
+ }
35
+
36
+ /**
37
+ * Partial transcripts as they arrive, ending when the session finishes
38
+ * (after {@link flush} or {@link cancel}). Each partial is cumulative:
39
+ * render it in place of the previous one. Use one iterator per session.
40
+ */
41
+ async *partials(): AsyncGenerator<PartialResult, void, void> {
42
+ for (;;) {
43
+ const partial = (await NativeXybrid.sessionNextPartial(this.handle)) as PartialResult | null;
44
+ if (partial == null) return;
45
+ yield partial;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Drain the buffered audio and return the complete transcript. The session
51
+ * is over afterwards.
52
+ */
53
+ flush(): Promise<string> {
54
+ return NativeXybrid.sessionFlush(this.handle);
55
+ }
56
+
57
+ /** Start over on fresh audio without reloading the model. */
58
+ reset(): Promise<void> {
59
+ return NativeXybrid.sessionReset(this.handle);
60
+ }
61
+
62
+ /** Stop and discard buffered audio — the "user walked away" path. */
63
+ cancel(): Promise<void> {
64
+ return NativeXybrid.sessionCancel(this.handle);
65
+ }
66
+
67
+ /** Whether the session still accepts audio. */
68
+ isRunning(): Promise<boolean> {
69
+ return NativeXybrid.sessionIsRunning(this.handle);
70
+ }
71
+
72
+ /** Free the native session (cancelling it if still running). */
73
+ release(): Promise<void> {
74
+ return NativeXybrid.dispose(this.handle);
75
+ }
76
+ }