arcane-os 0.94.0 → 0.96.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/ai/core-model-assets.mjs +74 -0
  4. package/browser-runtime/core/client.mjs +195 -0
  5. package/docs/reference/README.md +9 -0
  6. package/docs/reference/ai/browser-speech.md +61 -10
  7. package/docs/reference/core-client.md +35 -0
  8. package/docs/reference/core-native-packaging.md +38 -6
  9. package/docs/reference/inventory/package-api.json +133 -5
  10. package/docs/reference/inventory/runtime-modules.json +39 -13
  11. package/docs/reference/model-assets.md +164 -3
  12. package/docs/reference/native-magpie.md +98 -0
  13. package/docs/reference/native-nemotron.md +204 -0
  14. package/docs/reference/native-speech.md +250 -5
  15. package/docs/reference/runtime-modules.md +157 -11
  16. package/docs/reference/sdk-api.md +247 -4
  17. package/package.json +7 -3
  18. package/runtime/arcane/modules/AI.js +114 -14
  19. package/runtime/arcane/modules/AIProviderRuntime.js +32 -15
  20. package/runtime/arcane/modules/ContinuousVoiceCapture.js +118 -21
  21. package/runtime/arcane/modules/NativeSpeechCapture.js +198 -0
  22. package/runtime/arcane/modules/NativeSpeechPlayback.js +304 -0
  23. package/runtime/arcane/modules/VoiceCaptureWorklet.js +62 -2
  24. package/src/core/host.mjs +1 -0
  25. package/src/core/services/local-ai.mjs +46 -12
  26. package/src/core/services/model-assets.mjs +491 -59
  27. package/src/core/services/speech.mjs +237 -4
  28. package/src/local-ai/magpie/build.mjs +36 -0
  29. package/src/local-ai/magpie/helper.mjs +254 -0
  30. package/src/local-ai/magpie/index.mjs +488 -0
  31. package/src/local-ai/magpie/native/CMakeLists.txt +10 -0
  32. package/src/local-ai/magpie/native/main.cpp +270 -0
  33. package/src/local-ai/nemo-models.mjs +160 -0
  34. package/src/local-ai/nemotron/build.mjs +53 -0
  35. package/src/local-ai/nemotron/helper.mjs +308 -0
  36. package/src/local-ai/nemotron/index.mjs +693 -0
  37. package/src/local-ai/nemotron/native/CMakeLists.txt +17 -0
  38. package/src/local-ai/nemotron/native/main.cpp +282 -0
  39. package/src/native/macos-provider.mjs +14 -7
  40. package/src/native/portable-assembly.mjs +180 -0
  41. package/src/native/portable-provider.mjs +17 -161
  42. package/src/native/publish-output.mjs +101 -0
  43. package/src/native/windows-provider.mjs +12 -6
@@ -0,0 +1,308 @@
1
+ import {PassThrough} from 'node:stream';
2
+ import path from 'node:path';
3
+ import {runProcess} from '../../process.mjs';
4
+
5
+ /** One owned process drives the public ASR C ABI on its single native thread. */
6
+ export function createNemotronHelper(
7
+ {runtime, modelPath, diarizationModelPath, gpu, onEvent}
8
+ ) {
9
+ const input = new PassThrough();
10
+ const controller = new AbortController();
11
+ const readiness = deferred();
12
+ const pending = new Map();
13
+ const running = new Set();
14
+ let fragment = '';
15
+ let sequence = 0;
16
+ let writeTail = Promise.resolve();
17
+ let stopping = false;
18
+ let stopped = null;
19
+ let failure = null;
20
+ let metadata = null;
21
+ let closingStream = null;
22
+
23
+ function rejectPending(error) {
24
+ const results = [];
25
+ for (const [request, operation] of pending) {
26
+ if (operation.results.length) {
27
+ results.push(
28
+ {request, results: operation.results}
29
+ );
30
+ }
31
+ }
32
+ const completeFailure = results.length || fragment !== ''
33
+ ? nemotronError(
34
+ error.code ?? 'NEMOTRON_HELPER_FAILED',
35
+ error.message ?? 'The Nemotron helper failed.',
36
+ {cause: error, pendingResults: results, nativeFragment: fragment}
37
+ )
38
+ : error;
39
+ readiness.reject(completeFailure);
40
+ for (const operation of pending.values()) operation.response.reject(completeFailure);
41
+ pending.clear();
42
+ running.clear();
43
+ return completeFailure;
44
+ }
45
+
46
+ function receive({stream, chunk}) {
47
+ if (stream !== 'stdout') return;
48
+ fragment += chunk;
49
+ let boundary = fragment.indexOf('\n');
50
+ while (boundary !== -1) {
51
+ const line = fragment.substring(0, boundary);
52
+ fragment = fragment.substring(boundary + 1);
53
+ if (line !== '') {
54
+ let message;
55
+ try {
56
+ message = JSON.parse(line);
57
+ } catch (cause) {
58
+ throw nemotronError(
59
+ 'NEMOTRON_PROTOCOL_UNREADABLE',
60
+ 'The Nemotron helper returned an unreadable record.',
61
+ {line, cause}
62
+ );
63
+ }
64
+ if (message.ready) {
65
+ metadata = message;
66
+ readiness.resolve(message);
67
+ } else if (message.request === 0 && message.error) {
68
+ const error = nemotronError('NEMOTRON_MODEL_FAILED', message.error);
69
+ rejectPending(error);
70
+ } else {
71
+ const operation = pending.get(message.request);
72
+ if (!operation) {
73
+ throw nemotronError('NEMOTRON_PROTOCOL_UNKNOWN_REQUEST', 'The Nemotron helper returned an unknown request.', message);
74
+ }
75
+ if (message.running) {
76
+ running.add(message.request);
77
+ closingStream?.interrupt();
78
+ } else if (message.update) {
79
+ operation.results.push(message.update);
80
+ } else {
81
+ running.delete(message.request);
82
+ pending.delete(message.request);
83
+ if (message.error) {
84
+ operation.response.reject(
85
+ nemotronError(
86
+ 'NEMOTRON_NATIVE_FAILED',
87
+ message.error,
88
+ {message, results: operation.results}
89
+ )
90
+ );
91
+ } else {
92
+ operation.response.resolve(operation.results);
93
+ }
94
+ }
95
+ }
96
+ }
97
+ boundary = fragment.indexOf('\n');
98
+ }
99
+ }
100
+
101
+ const completion = runProcess(
102
+ runtime.helperExecutable,
103
+ [modelPath, diarizationModelPath ?? '', String(gpu)],
104
+ {
105
+ env: runtimeEnvironment(runtime),
106
+ signal: controller.signal,
107
+ input: {
108
+ [Symbol.asyncIterator]: function helperInput() {
109
+ const iterator = input[Symbol.asyncIterator]();
110
+ return {
111
+ next: function nextInput() { return iterator.next(); },
112
+ return: function closeInput() {
113
+ input.end();
114
+ return iterator.return();
115
+ }
116
+ };
117
+ }
118
+ },
119
+ onOutput: receive,
120
+ captureOutput: {stdout: false, stderr: true},
121
+ emitOutputEvents: {stdout: false, stderr: true},
122
+ onEvent
123
+ }
124
+ ).then(
125
+ function helperExited(result) {
126
+ if (fragment !== '') {
127
+ throw nemotronError(
128
+ 'NEMOTRON_PROTOCOL_INCOMPLETE',
129
+ 'The Nemotron helper exited during a record.',
130
+ {fragment, result}
131
+ );
132
+ }
133
+ if (!stopping || pending.size !== 0 || !metadata) {
134
+ throw nemotronError('NEMOTRON_HELPER_EXITED', 'The Nemotron helper exited before its operation completed.', result);
135
+ }
136
+ return result;
137
+ }
138
+ ).catch(
139
+ function helperFailed(error) {
140
+ failure = rejectPending(error);
141
+ throw failure;
142
+ }
143
+ ).finally(
144
+ function helperDrained() {
145
+ input.end();
146
+ }
147
+ );
148
+ completion.catch(
149
+ function observeHelperFailure() {}
150
+ );
151
+
152
+ function send(operation, parameters = '', payload) {
153
+ const response = deferred();
154
+ const id = ++sequence;
155
+ if (failure || stopping) {
156
+ response.reject(failure ?? nemotronError('NEMOTRON_HELPER_CLOSING', 'The Nemotron helper is closing.'));
157
+ return {response: response.promise, written: Promise.resolve()};
158
+ }
159
+ pending.set(
160
+ id,
161
+ {response, operation, results: []}
162
+ );
163
+ const written = writeTail.then(
164
+ async function writeCommand() {
165
+ if (failure) throw failure;
166
+ await writeInput(input, `${operation} ${id}${parameters ? ` ${parameters}` : ''}\n`);
167
+ if (payload !== undefined) await writeInput(input, payload);
168
+ }
169
+ );
170
+ writeTail = written.catch(
171
+ function commandWriteFailed(error) {
172
+ failure = rejectPending(error);
173
+ input.end();
174
+ controller.abort(failure);
175
+ }
176
+ );
177
+ return {response: response.promise, written};
178
+ }
179
+
180
+ function command(operation, parameters, payload) {
181
+ return send(operation, parameters, payload).response;
182
+ }
183
+
184
+ function stop(reason) {
185
+ if (stopped) return stopped;
186
+ stopping = true;
187
+ // EOF is the ordinary native shutdown path. Only a call already inside
188
+ // the noninterruptible ABI (including model creation) needs termination.
189
+ const noninterruptible = !metadata || running.size !== 0;
190
+ if (noninterruptible) {
191
+ input.end();
192
+ controller.abort(reason);
193
+ }
194
+ async function stopOwnedHelper() {
195
+ if (!noninterruptible) {
196
+ await writeTail;
197
+ input.end();
198
+ }
199
+ return completion;
200
+ }
201
+ stopped = stopOwnedHelper().catch(
202
+ function stoppedHelper(error) {
203
+ if (controller.signal.aborted && error.code === 'ARCANE_CANCELLED') {
204
+ return {cancelled: true, resetRequired: true, diagnostics: error};
205
+ }
206
+ throw error;
207
+ }
208
+ );
209
+ return stopped;
210
+ }
211
+
212
+ async function closeStream(reason) {
213
+ if (failure) throw failure;
214
+ // Attempt the public stream close first. A running decode cannot read
215
+ // that command until it returns; terminate only this owned process then.
216
+ const closing = send('close');
217
+ let written = false;
218
+ let interrupted = false;
219
+ const cancellation = {
220
+ interrupt: function interruptActiveDecode() {
221
+ if (!written || running.size === 0 || interrupted) return;
222
+ interrupted = true;
223
+ stop(reason ?? nemotronError('NEMOTRON_CANCELLED', 'The active Nemotron decode was cancelled.')).catch(
224
+ function observeOwnedStop() {}
225
+ );
226
+ }
227
+ };
228
+ closingStream = cancellation;
229
+ try {
230
+ await closing.written;
231
+ written = true;
232
+ cancellation.interrupt();
233
+ try {
234
+ await closing.response;
235
+ } catch (error) {
236
+ if (!interrupted) throw error;
237
+ }
238
+ if (interrupted) return {resetRequired: true, diagnostics: await stopped};
239
+ return {resetRequired: false};
240
+ } finally {
241
+ if (closingStream === cancellation) closingStream = null;
242
+ }
243
+ }
244
+
245
+ return {
246
+ ready: readiness.promise,
247
+ completion,
248
+ command,
249
+ closeStream,
250
+ stop,
251
+ current: function helperState() { return {metadata, failure, running: running.size !== 0, stopping}; }
252
+ };
253
+ }
254
+
255
+ export function encodeNemotronAudio(samples) {
256
+ // This is only the native transport's little-endian float32 framing.
257
+ const encoded = Buffer.allocUnsafe(samples.length * 4);
258
+ const bits = new Uint32Array(samples.buffer, samples.byteOffset, samples.length);
259
+ for (let index = 0; index < samples.length; index++) encoded.writeUInt32LE(bits[index], index * 4);
260
+ return encoded;
261
+ }
262
+
263
+ function runtimeEnvironment(runtime) {
264
+ const environment = {...process.env, ...runtime.env};
265
+ const key = process.platform === 'win32'
266
+ ? Object.keys(environment).find(
267
+ function pathKey(name) { return name.toLowerCase() === 'path'; }
268
+ ) ?? 'PATH'
269
+ : process.platform === 'darwin' ? 'DYLD_LIBRARY_PATH' : 'LD_LIBRARY_PATH';
270
+ const directories = [runtime.binaryDirectory, runtime.libraryDirectory].filter(Boolean);
271
+ if (directories.length) environment[key] = [...directories, environment[key]].filter(Boolean).join(path.delimiter);
272
+ return environment;
273
+ }
274
+
275
+ function writeInput(input, value) {
276
+ return new Promise(
277
+ function inputWrite(resolve, reject) {
278
+ input.write(
279
+ value,
280
+ function inputWritten(error) {
281
+ if (error) reject(error);
282
+ else resolve();
283
+ }
284
+ );
285
+ }
286
+ );
287
+ }
288
+
289
+ export function deferred() {
290
+ const result = {};
291
+ result.promise = new Promise(
292
+ function pending(resolve, reject) {
293
+ result.resolve = resolve;
294
+ result.reject = reject;
295
+ }
296
+ );
297
+ result.promise.catch(
298
+ function observePendingFailure() {}
299
+ );
300
+ return result;
301
+ }
302
+
303
+ export function nemotronError(code, message, details) {
304
+ const error = new Error(message);
305
+ error.code = code;
306
+ if (details !== undefined) error.details = details;
307
+ return error;
308
+ }