arcane-os 0.93.0 → 0.95.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 (42) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/ai/twin-cloud.mjs +109 -46
  4. package/browser-runtime/core/client.mjs +197 -0
  5. package/docs/reference/ai/twin-cloud.md +35 -14
  6. package/docs/reference/availability-and-normalization.md +1 -1
  7. package/docs/reference/inventory/package-api.json +96 -12
  8. package/docs/reference/local-ai.md +81 -5
  9. package/docs/reference/native-magpie.md +98 -0
  10. package/docs/reference/native-nemotron.md +204 -0
  11. package/docs/reference/native-speech.md +364 -11
  12. package/docs/reference/runtime-modules.md +21 -18
  13. package/docs/reference/sdk-api.md +152 -11
  14. package/package.json +6 -1
  15. package/runtime/arcane/modules/AI.js +114 -14
  16. package/runtime/arcane/modules/AIProviderRuntime.js +32 -15
  17. package/runtime/arcane/modules/ContinuousVoiceCapture.js +118 -21
  18. package/runtime/arcane/modules/NativeSpeechCapture.js +198 -0
  19. package/runtime/arcane/modules/NativeSpeechPlayback.js +304 -0
  20. package/runtime/arcane/modules/VoiceCaptureWorklet.js +62 -2
  21. package/schemas/arcane-app.schema.json +2 -2
  22. package/schemas/arcane-package.schema.json +2 -2
  23. package/src/core/services/speech.mjs +320 -25
  24. package/src/local-ai/config.mjs +12 -1
  25. package/src/local-ai/dev.mjs +71 -4
  26. package/src/local-ai/install.mjs +113 -5
  27. package/src/local-ai/kokoro/frontend.mjs +219 -0
  28. package/src/local-ai/kokoro/helper.mjs +168 -0
  29. package/src/local-ai/kokoro/index.mjs +645 -0
  30. package/src/local-ai/kokoro/native/CMakeLists.txt +45 -0
  31. package/src/local-ai/kokoro/native/main.cpp +249 -0
  32. package/src/local-ai/magpie/build.mjs +36 -0
  33. package/src/local-ai/magpie/helper.mjs +254 -0
  34. package/src/local-ai/magpie/index.mjs +488 -0
  35. package/src/local-ai/magpie/native/CMakeLists.txt +10 -0
  36. package/src/local-ai/magpie/native/main.cpp +270 -0
  37. package/src/local-ai/nemotron/build.mjs +53 -0
  38. package/src/local-ai/nemotron/helper.mjs +308 -0
  39. package/src/local-ai/nemotron/index.mjs +637 -0
  40. package/src/local-ai/nemotron/native/CMakeLists.txt +17 -0
  41. package/src/local-ai/nemotron/native/main.cpp +282 -0
  42. package/src/native/portable-layout.mjs +67 -9
@@ -0,0 +1,45 @@
1
+ cmake_minimum_required(VERSION 3.26)
2
+ project(ArcaneKokoroFrontend LANGUAGES CXX)
3
+
4
+ # The producer builds the disclosed upstream projects through their own build systems.
5
+ # This target consumes their public headers/static libraries; it acquires nothing.
6
+ set(ESPEAK_NG_INCLUDE_DIRECTORY "" CACHE PATH "Public include directory containing espeak-ng/speak_lib.h")
7
+ set(ESPEAK_NG_LIBRARY "" CACHE FILEPATH "Matching static espeak-ng library")
8
+ set(UCD_LIBRARY "" CACHE FILEPATH "Matching eSpeak Unicode character database static library")
9
+ set(OPUS_INCLUDE_DIRECTORY "" CACHE PATH "Public include directory containing opus.h")
10
+ set(OPUS_LIBRARY "" CACHE FILEPATH "Disclosed libopus 1.6.1 static library")
11
+ set(OPUSENC_INCLUDE_DIRECTORY "" CACHE PATH "Public include directory containing opusenc.h")
12
+ set(OPUSENC_LIBRARY "" CACHE FILEPATH "Disclosed libopusenc 0.3 static library")
13
+ set(ESPEAK_NG_DATA_DIRECTORY "" CACHE PATH "Complete generated same-revision espeak-ng-data directory")
14
+
15
+ add_executable(arcane-kokoro main.cpp)
16
+ target_compile_features(arcane-kokoro PRIVATE cxx_std_17)
17
+ target_compile_definitions(arcane-kokoro PRIVATE LIBESPEAK_NG_EXPORT=1)
18
+ if(WIN32)
19
+ target_compile_definitions(arcane-kokoro PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN)
20
+ endif()
21
+ target_include_directories(arcane-kokoro PRIVATE
22
+ "${ESPEAK_NG_INCLUDE_DIRECTORY}" "${OPUS_INCLUDE_DIRECTORY}" "${OPUSENC_INCLUDE_DIRECTORY}")
23
+ target_link_libraries(arcane-kokoro PRIVATE
24
+ "${ESPEAK_NG_LIBRARY}" "${UCD_LIBRARY}" "${OPUSENC_LIBRARY}" "${OPUS_LIBRARY}")
25
+ if(MSVC)
26
+ target_compile_options(arcane-kokoro PRIVATE /utf-8 /EHsc)
27
+ elseif(UNIX)
28
+ target_link_libraries(arcane-kokoro PRIVATE m)
29
+ elseif(MINGW)
30
+ target_link_options(arcane-kokoro PRIVATE -municode)
31
+ endif()
32
+ install(TARGETS arcane-kokoro RUNTIME DESTINATION bin)
33
+ if(ESPEAK_NG_DATA_DIRECTORY)
34
+ install(DIRECTORY "${ESPEAK_NG_DATA_DIRECTORY}/" DESTINATION share/espeak-ng-data)
35
+ endif()
36
+
37
+ if(WIN32 AND MSVC)
38
+ set(CMAKE_INSTALL_SYSTEM_RUNTIME_DESTINATION bin)
39
+ set(CMAKE_INSTALL_DEBUG_LIBRARIES FALSE)
40
+ set(CMAKE_INSTALL_DEBUG_LIBRARIES_ONLY FALSE)
41
+ set(CMAKE_INSTALL_UCRT_LIBRARIES FALSE)
42
+ set(CMAKE_INSTALL_MFC_LIBRARIES FALSE)
43
+ set(CMAKE_INSTALL_OPENMP_LIBRARIES FALSE)
44
+ include(InstallRequiredSystemLibraries)
45
+ endif()
@@ -0,0 +1,249 @@
1
+ #include <espeak-ng/speak_lib.h>
2
+ #include <opusenc.h>
3
+
4
+ #include <array>
5
+ #include <cstdint>
6
+ #include <cstring>
7
+ #include <filesystem>
8
+ #include <iostream>
9
+ #include <limits>
10
+ #include <sstream>
11
+ #include <stdexcept>
12
+ #include <string>
13
+ #include <vector>
14
+
15
+ #ifdef _WIN32
16
+ #include <fcntl.h>
17
+ #include <io.h>
18
+ #include <windows.h>
19
+ #endif
20
+
21
+ // This private pipe uses length framing solely to transport complete text and float PCM.
22
+ // No inference runtime is linked into this helper.
23
+ static void send(const std::string &kind, const std::string &id, const char *data, std::size_t length) {
24
+ std::cout << kind << '\t' << id << '\t' << length << '\n';
25
+ std::size_t offset = 0;
26
+ while (offset < length) {
27
+ const auto remaining = length - offset;
28
+ const auto portion = remaining > static_cast<std::size_t>(std::numeric_limits<std::streamsize>::max())
29
+ ? std::numeric_limits<std::streamsize>::max() : static_cast<std::streamsize>(remaining);
30
+ std::cout.write(data + offset, portion);
31
+ offset += static_cast<std::size_t>(portion);
32
+ }
33
+ std::cout.flush();
34
+ if (!std::cout) throw std::runtime_error("Writing the complete Kokoro helper response failed.");
35
+ }
36
+
37
+ static void send(const std::string &kind, const std::string &id, const std::string &data = {}) {
38
+ send(kind, id, data.data(), data.length());
39
+ }
40
+
41
+ static void receive(char *data, std::size_t length) {
42
+ std::size_t offset = 0;
43
+ while (offset < length) {
44
+ const auto remaining = length - offset;
45
+ const auto portion = remaining > static_cast<std::size_t>(std::numeric_limits<std::streamsize>::max())
46
+ ? std::numeric_limits<std::streamsize>::max() : static_cast<std::streamsize>(remaining);
47
+ std::cin.read(data + offset, portion);
48
+ if (std::cin.gcount() != portion) throw std::runtime_error("The Kokoro command ended before its complete payload arrived.");
49
+ offset += static_cast<std::size_t>(portion);
50
+ }
51
+ }
52
+
53
+ static std::string jsonString(const std::string &text) {
54
+ const char *hex = "0123456789abcdef";
55
+ std::string result = "\"";
56
+ for (const unsigned char character : text) {
57
+ if (character == '\"' || character == '\\') {
58
+ result += '\\';
59
+ result += static_cast<char>(character);
60
+ } else if (character < 0x20) {
61
+ result += "\\u00";
62
+ result += hex[character >> 4];
63
+ result += hex[character & 15];
64
+ } else result += static_cast<char>(character);
65
+ }
66
+ result += '\"';
67
+ return result;
68
+ }
69
+
70
+ static void selectLanguage(const std::string &language) {
71
+ espeak_VOICE properties{};
72
+ properties.languages = language.c_str();
73
+ if (espeak_SetVoiceByProperties(&properties) != EE_OK) throw std::runtime_error("eSpeak could not select " + language + ".");
74
+ const espeak_VOICE *voice = espeak_GetCurrentVoice();
75
+ // Public languages records consist of priority, language, NUL, repeated, then NUL.
76
+ const char *languages = voice ? voice->languages : nullptr;
77
+ while (languages && *languages) {
78
+ ++languages;
79
+ if (language == languages) return;
80
+ languages += std::strlen(languages) + 1;
81
+ }
82
+ throw std::runtime_error("The matching eSpeak language data for " + language + " is unavailable.");
83
+ }
84
+
85
+ static void phonemize(const std::string &id, std::size_t length, const std::string &language) {
86
+ std::string text(length, '\0');
87
+ receive(text.data(), text.length());
88
+ if (text.find('\0') != std::string::npos) throw std::runtime_error("The public eSpeak text interface cannot consume embedded NUL characters.");
89
+ selectLanguage(language);
90
+ const void *cursor = text.c_str();
91
+ const char *end = text.c_str() + text.length();
92
+ while (cursor) {
93
+ const char *begin = static_cast<const char *>(cursor);
94
+ const char *phonemes = espeak_TextToPhonemes(&cursor, espeakCHARS_UTF8,
95
+ espeakPHONEMES_IPA | espeakPHONEMES_TIE | ('^' << 8));
96
+ if (!phonemes) throw std::runtime_error("eSpeak did not return phonemes for the current clause.");
97
+ // Copy the library-owned result before calling eSpeak again.
98
+ const std::string translated(phonemes);
99
+ const char *next = cursor ? static_cast<const char *>(cursor) : end;
100
+ if (next < begin || next > end || (cursor && next == begin)) {
101
+ throw std::runtime_error("eSpeak did not advance through the supplied text.");
102
+ }
103
+ const std::string consumed(begin, next);
104
+ send("clause", id, "{\"source\":" + jsonString(consumed) + ",\"phonemes\":" + jsonString(translated) + "}");
105
+ }
106
+ send("done", id);
107
+ }
108
+
109
+ struct EncoderOutput {
110
+ std::string id;
111
+ std::string error;
112
+ };
113
+
114
+ static int writePage(void *context, const unsigned char *data, opus_int32 length) {
115
+ auto &output = *static_cast<EncoderOutput *>(context);
116
+ try {
117
+ send("page", output.id, reinterpret_cast<const char *>(data), static_cast<std::size_t>(length));
118
+ return 0;
119
+ } catch (const std::exception &error) {
120
+ output.error = error.what();
121
+ return 1;
122
+ }
123
+ }
124
+
125
+ static int closeEncoder(void *) { return 0; }
126
+
127
+ static void encodeOpus(const std::string &id, std::size_t length) {
128
+ if (length % sizeof(float) != 0) throw std::runtime_error("The Opus input is not complete float32 PCM framing.");
129
+ EncoderOutput output{id, {}};
130
+ const OpusEncCallbacks callbacks{writePage, closeEncoder};
131
+ OggOpusComments *comments = ope_comments_create();
132
+ if (!comments) throw std::runtime_error("Creating Opus comments failed.");
133
+ int error = OPE_OK;
134
+ OggOpusEnc *encoder = ope_encoder_create_callbacks(&callbacks, &output, comments, 24000, 1, 0, &error);
135
+ ope_comments_destroy(comments);
136
+ if (!encoder) throw std::runtime_error(ope_strerror(error));
137
+ try {
138
+ std::array<unsigned char, 4096 * 4> encoded{};
139
+ std::array<float, 4096> samples{};
140
+ std::size_t remaining = length / 4;
141
+ while (remaining) {
142
+ const auto count = remaining < samples.size() ? remaining : samples.size();
143
+ receive(reinterpret_cast<char *>(encoded.data()), count * 4);
144
+ for (std::size_t index = 0; index < count; ++index) {
145
+ const auto offset = index * 4;
146
+ const std::uint32_t bits = static_cast<std::uint32_t>(encoded[offset])
147
+ | (static_cast<std::uint32_t>(encoded[offset + 1]) << 8)
148
+ | (static_cast<std::uint32_t>(encoded[offset + 2]) << 16)
149
+ | (static_cast<std::uint32_t>(encoded[offset + 3]) << 24);
150
+ std::memcpy(&samples[index], &bits, sizeof(float));
151
+ }
152
+ error = ope_encoder_write_float(encoder, samples.data(), static_cast<int>(count));
153
+ if (error != OPE_OK) throw std::runtime_error(ope_strerror(error));
154
+ remaining -= count;
155
+ }
156
+ error = ope_encoder_drain(encoder);
157
+ if (error != OPE_OK) throw std::runtime_error(ope_strerror(error));
158
+ if (!output.error.empty()) throw std::runtime_error(output.error);
159
+ ope_encoder_destroy(encoder);
160
+ encoder = nullptr;
161
+ send("done", id);
162
+ } catch (...) {
163
+ if (encoder) ope_encoder_destroy(encoder);
164
+ throw;
165
+ }
166
+ }
167
+
168
+ static int run(const std::vector<std::string> &arguments) {
169
+ std::string data;
170
+ if (arguments.size() == 3 && arguments[1] == "--data") data = arguments[2];
171
+ else throw std::runtime_error("Kokoro requires --data with the parent of the matching espeak-ng-data directory.");
172
+ if (espeak_Initialize(AUDIO_OUTPUT_SYNCHRONOUS, 0, data.c_str(), espeakINITIALIZE_DONT_EXIT) < 0) {
173
+ throw std::runtime_error("Initializing the matching eSpeak language data failed.");
174
+ }
175
+ try {
176
+ // eSpeak may choose an environment/installed data directory when the supplied path is absent.
177
+ // Read its public selection so helper readiness describes the configured resources.
178
+ const char *selectedData = nullptr;
179
+ espeak_Info(&selectedData);
180
+ const auto expectedData = (std::filesystem::u8path(data) / "espeak-ng-data").lexically_normal();
181
+ if (!selectedData || std::filesystem::u8path(selectedData).lexically_normal() != expectedData) {
182
+ throw std::runtime_error("eSpeak selected a different data directory: " + std::string(selectedData ? selectedData : "<none>")
183
+ + "; requested " + expectedData.u8string());
184
+ }
185
+ // Resolve both supported dialects before reporting helper readiness.
186
+ selectLanguage("en-us");
187
+ selectLanguage("en-gb");
188
+ send("ready", "0");
189
+ std::string header;
190
+ while (std::getline(std::cin, header)) {
191
+ std::istringstream fields(header);
192
+ std::string kind, id, lengthText, language;
193
+ std::getline(fields, kind, '\t');
194
+ std::getline(fields, id, '\t');
195
+ std::getline(fields, lengthText, '\t');
196
+ std::getline(fields, language);
197
+ try {
198
+ const auto length = std::stoull(lengthText);
199
+ if (length > std::numeric_limits<std::size_t>::max()) throw std::runtime_error("The helper platform cannot represent this transport frame.");
200
+ if (kind == "phonemize") phonemize(id, static_cast<std::size_t>(length), language);
201
+ else if (kind == "opus") encodeOpus(id, static_cast<std::size_t>(length));
202
+ else throw std::runtime_error("Unknown Kokoro helper command: " + kind);
203
+ } catch (const std::exception &error) {
204
+ send("error", id, error.what());
205
+ throw;
206
+ }
207
+ }
208
+ if (!std::cin.eof()) throw std::runtime_error("Reading the Kokoro command stream failed.");
209
+ espeak_Terminate();
210
+ return 0;
211
+ } catch (...) {
212
+ espeak_Terminate();
213
+ throw;
214
+ }
215
+ }
216
+
217
+ #ifdef _WIN32
218
+ static std::string utf8(const wchar_t *text) {
219
+ const int length = WideCharToMultiByte(CP_UTF8, 0, text, -1, nullptr, 0, nullptr, nullptr);
220
+ if (!length) throw std::runtime_error("Reading the Unicode Kokoro helper argument failed.");
221
+ std::string result(static_cast<std::size_t>(length), '\0');
222
+ if (!WideCharToMultiByte(CP_UTF8, 0, text, -1, result.data(), length, nullptr, nullptr)) {
223
+ throw std::runtime_error("Converting the Unicode Kokoro helper argument failed.");
224
+ }
225
+ result.pop_back();
226
+ return result;
227
+ }
228
+
229
+ int wmain(int argc, wchar_t **argv) {
230
+ _setmode(_fileno(stdin), _O_BINARY);
231
+ _setmode(_fileno(stdout), _O_BINARY);
232
+ #else
233
+ int main(int argc, char **argv) {
234
+ #endif
235
+ try {
236
+ std::vector<std::string> arguments;
237
+ for (int index = 0; index < argc; ++index) {
238
+ #ifdef _WIN32
239
+ arguments.push_back(utf8(argv[index]));
240
+ #else
241
+ arguments.emplace_back(argv[index]);
242
+ #endif
243
+ }
244
+ return run(arguments);
245
+ } catch (const std::exception &error) {
246
+ std::cerr << error.what() << '\n';
247
+ return 1;
248
+ }
249
+ }
@@ -0,0 +1,36 @@
1
+ import path from 'node:path';
2
+ import {fileURLToPath} from 'node:url';
3
+ import {mkdir} from 'node:fs/promises';
4
+ import Is from 'strong-type';
5
+ import {runProcess} from '../../process.mjs';
6
+
7
+ const is = new Is(false);
8
+
9
+ /** Build the first-party adapter against an explicitly prepared NeMo TTS runtime. */
10
+ export async function buildMagpieHelper({runtime, outputRoot, signal, onEvent} = {}) {
11
+ if (!is.string(runtime?.cmakeDirectory) || !is.string(outputRoot)) {
12
+ throw new TypeError('Magpie requires the selected NeMo runtime cmakeDirectory and helper outputRoot.');
13
+ }
14
+ const root = path.resolve(outputRoot);
15
+ const build = path.join(root, 'build');
16
+ const source = fileURLToPath(new URL('./native/', import.meta.url));
17
+ await mkdir(root, {recursive: true});
18
+ const options = {signal, onEvent};
19
+ const configure = await runProcess(
20
+ 'cmake',
21
+ ['-S', source, '-B', build, `-DNeMoSpeech_DIR=${path.resolve(runtime.cmakeDirectory)}`,
22
+ `-DCMAKE_INSTALL_PREFIX=${root}`, '-DCMAKE_BUILD_TYPE=Release'],
23
+ options
24
+ );
25
+ const compile = await runProcess('cmake', ['--build', build, '--config', 'Release'], options);
26
+ const install = await runProcess('cmake', ['--install', build, '--config', 'Release'], options);
27
+ return {
28
+ platform: process.platform,
29
+ architecture: process.arch,
30
+ root,
31
+ helperExecutable: path.join(root, 'bin', process.platform === 'win32' ? 'arcane-magpie.exe' : 'arcane-magpie'),
32
+ diagnostics: {configure, compile, install}
33
+ };
34
+ }
35
+
36
+ export default buildMagpieHelper;
@@ -0,0 +1,254 @@
1
+ import path from 'node:path';
2
+ import {PassThrough} from 'node:stream';
3
+ import Is from 'strong-type';
4
+ import {runProcess} from '../../process.mjs';
5
+
6
+ const is = new Is(false);
7
+
8
+ /** One retained native synthesizer; the host owns playback and audio devices. */
9
+ export function createMagpieHelper({runtime, paths, speaker, executionTarget, onEvent}) {
10
+ const input = new PassThrough();
11
+ const lifetime = new AbortController();
12
+ const readiness = deferred();
13
+ let sequence = 0;
14
+ let pending = null;
15
+ let metadata = null;
16
+ let fragment = Buffer.alloc(0);
17
+ let frame = null;
18
+ let writes = Promise.resolve();
19
+ let stopping = false;
20
+ let exited = false;
21
+ let terminalError = null;
22
+ let closing = null;
23
+ const env = {...process.env};
24
+ const directory = runtime.libraryDirectory ?? path.dirname(runtime.helperExecutable);
25
+ const variable = process.platform === 'win32'
26
+ ? Object.keys(env).find(function pathVariable(name) { return name.toLowerCase() === 'path'; }) ?? 'PATH'
27
+ : process.platform === 'darwin' ? 'DYLD_LIBRARY_PATH' : 'LD_LIBRARY_PATH';
28
+ env[variable] = [directory, env[variable]].filter(Boolean).join(path.delimiter);
29
+ // The native device owner supplies the selected CUDA UUID/visible device.
30
+ // This adapter neither invents a device ordinal nor retries on another device.
31
+ if (executionTarget.cudaVisibleDevices !== undefined) {
32
+ env.CUDA_VISIBLE_DEVICES = executionTarget.cudaVisibleDevices;
33
+ }
34
+ const iterator = input[Symbol.asyncIterator]();
35
+ const incoming = {
36
+ [Symbol.asyncIterator]() { return this; },
37
+ next() { return iterator.next(); },
38
+ return() { input.end(); return iterator.return(); }
39
+ };
40
+ const completion = runProcess(
41
+ runtime.helperExecutable,
42
+ [paths.model, paths.codec, paths.tokenizerDirectory, String(speaker), executionTarget.backend],
43
+ {
44
+ env, signal: lifetime.signal, input: incoming, onEvent,
45
+ outputEncoding: {stdout: null, stderr: 'utf8'},
46
+ captureOutput: {stdout: false, stderr: true},
47
+ emitOutputEvents: {stdout: false, stderr: true},
48
+ onOutput: receive
49
+ }
50
+ ).then(function processExited(result) {
51
+ if (fragment.length || frame) throw failure('MAGPIE_PROTOCOL_INCOMPLETE', 'The Magpie helper exited during an output frame.');
52
+ if (!stopping || (pending && !pending.terminal)) throw failure('MAGPIE_HELPER_EXITED', 'The Magpie helper exited before its work completed.', result);
53
+ return result;
54
+ }).catch(function processFailed(error) {
55
+ terminalError = error;
56
+ throw error;
57
+ }).finally(function processSettled() {
58
+ exited = true;
59
+ input.end();
60
+ const error = terminalError ?? failure('MAGPIE_HELPER_CLOSED', 'The Magpie helper has closed.');
61
+ readiness.reject(error);
62
+ pending?.response.reject(error);
63
+ });
64
+ completion.catch(function observeProcessFailure() {});
65
+
66
+ async function receive({stream, chunk}) {
67
+ if (stream !== 'stdout') return;
68
+ fragment = fragment.length ? Buffer.concat([fragment, chunk]) : chunk;
69
+ for (;;) {
70
+ if (!frame) {
71
+ const boundary = fragment.indexOf(10);
72
+ if (boundary < 0) return;
73
+ const [kind, id, encodedLength] = fragment.subarray(0, boundary).toString('utf8').split('\t');
74
+ const length = Number(encodedLength);
75
+ if (!is.safeInteger(length) || length < 0) {
76
+ throw failure('MAGPIE_PROTOCOL_UNREADABLE', 'The Magpie helper returned unreadable transport framing.');
77
+ }
78
+ frame = {kind, id, length};
79
+ fragment = fragment.subarray(boundary + 1);
80
+ }
81
+ if (fragment.length < frame.length) return;
82
+ const payload = fragment.subarray(0, frame.length);
83
+ fragment = fragment.subarray(frame.length);
84
+ const record = frame;
85
+ frame = null;
86
+ if (record.kind === 'fatal') {
87
+ throw failure('MAGPIE_NATIVE_FAILED', payload.toString('utf8'));
88
+ }
89
+ if (record.kind === 'ready') {
90
+ const sampleRate = Number(payload.toString('utf8'));
91
+ if (!is.integer(sampleRate) || sampleRate <= 0) {
92
+ throw failure('MAGPIE_PROTOCOL_UNREADABLE', 'The native synthesizer returned an unreadable sample rate.');
93
+ }
94
+ metadata = {sampleRate, channels: 1, format: 's16le'};
95
+ readiness.resolve(metadata);
96
+ continue;
97
+ }
98
+ const operation = pending;
99
+ if (!operation || record.id !== operation.id) {
100
+ throw failure('MAGPIE_PROTOCOL_UNKNOWN_REQUEST', `The Magpie helper returned ${record.kind} without its active request.`);
101
+ }
102
+ if (record.kind === 'audio' || record.kind === 'started') {
103
+ if (operation.signal?.aborted || operation.callbackError) continue;
104
+ try {
105
+ if (record.kind === 'audio') {
106
+ // Awaited delivery lets runProcess pause stdout and native PCM production.
107
+ await operation.onAudio({pcm: Buffer.from(payload), ...metadata});
108
+ } else {
109
+ await operation.onProgress?.({phase: 'synthesizing'});
110
+ }
111
+ } catch (error) {
112
+ operation.callbackError = error;
113
+ try { await writeCommand([Buffer.from(`stop\t${operation.id}\n`)]); }
114
+ catch (stopError) {
115
+ throw new AggregateError([error, stopError], 'The Magpie consumer and native cancellation failed.');
116
+ }
117
+ }
118
+ } else if (record.kind === 'error') {
119
+ operation.terminal = true;
120
+ const nativeError = failure('MAGPIE_NATIVE_FAILED', payload.toString('utf8'));
121
+ const previous = operation.callbackError ?? operation.signal?.reason;
122
+ operation.response.reject(previous
123
+ ? new AggregateError([previous, nativeError], 'Magpie synthesis and its consumer failed.') : nativeError);
124
+ } else if (record.kind === 'cancelled') {
125
+ operation.terminal = true;
126
+ operation.response.reject(operation.callbackError ?? operation.signal?.reason
127
+ ?? failure('MAGPIE_SYNTHESIS_CANCELLED', 'The native synthesizer cancelled this utterance.'));
128
+ } else if (record.kind === 'done') {
129
+ operation.terminal = true;
130
+ if (operation.callbackError) operation.response.reject(operation.callbackError);
131
+ else if (operation.signal?.aborted) operation.response.reject(operation.signal.reason);
132
+ else operation.response.resolve(metadata);
133
+ } else {
134
+ throw failure('MAGPIE_PROTOCOL_UNREADABLE', `The Magpie helper returned an unknown record ${record.kind}.`);
135
+ }
136
+ }
137
+ }
138
+
139
+ function writeCommand(parts) {
140
+ const writing = writes.then(async function writeFramedCommand() {
141
+ if (exited || lifetime.signal.aborted) throw terminalError ?? lifetime.signal.reason
142
+ ?? failure('MAGPIE_HELPER_CLOSED', 'The Magpie helper has closed.');
143
+ for (const part of parts) {
144
+ await new Promise(function writeInput(resolve, reject) {
145
+ input.write(part, function inputWritten(error) {
146
+ if (error) reject(error);
147
+ else resolve();
148
+ });
149
+ });
150
+ }
151
+ });
152
+ writes = writing.catch(function commandFailed(error) {
153
+ // Incomplete framing cannot be reused. Retire only this owned process.
154
+ terminalError = error;
155
+ stopping = true;
156
+ lifetime.abort(error);
157
+ });
158
+ return writing;
159
+ }
160
+
161
+ async function synthesize({text, languageCode, speaker: selectedSpeaker}, {signal, onAudio, onProgress} = {}) {
162
+ signal?.throwIfAborted();
163
+ await readiness.promise;
164
+ signal?.throwIfAborted();
165
+ if (stopping || exited || pending) {
166
+ throw terminalError ?? failure('MAGPIE_HELPER_UNAVAILABLE', 'The Magpie helper is unavailable for another utterance.');
167
+ }
168
+ const operation = {
169
+ id: String(++sequence), response: deferred(), signal, onAudio, onProgress, callbackError: null, terminal: false
170
+ };
171
+ pending = operation;
172
+ function stopUtterance() {
173
+ writeCommand([Buffer.from(`stop\t${operation.id}\n`)]).catch(function stopFailed(error) {
174
+ terminalError = error;
175
+ });
176
+ }
177
+ const language = Buffer.from(languageCode, 'utf8');
178
+ const content = Buffer.from(text, 'utf8');
179
+ try {
180
+ // Install cancellation after enqueueing the complete text frame, so a
181
+ // concurrent stop cannot precede its own native synthesis command.
182
+ const writing = writeCommand([
183
+ Buffer.from(`synthesize\t${operation.id}\t${selectedSpeaker}\t${language.length}\t${content.length}\n`),
184
+ language, content
185
+ ]);
186
+ signal?.addEventListener('abort', stopUtterance, {once: true});
187
+ if (signal?.aborted) stopUtterance();
188
+ await writing;
189
+ return await operation.response.promise;
190
+ } catch (error) {
191
+ if (lifetime.signal.aborted || exited) {
192
+ try { await completion; }
193
+ catch (processError) {
194
+ if (processError !== error) throw new AggregateError([error, processError], 'Magpie synthesis and process cleanup failed.');
195
+ }
196
+ }
197
+ throw error;
198
+ } finally {
199
+ signal?.removeEventListener('abort', stopUtterance);
200
+ if (pending === operation) pending = null;
201
+ }
202
+ }
203
+
204
+ function close() {
205
+ if (closing) return closing;
206
+ stopping = true;
207
+ closing = Promise.resolve().then(async function closeHelper() {
208
+ if (!exited) {
209
+ if (!metadata) {
210
+ // No C-ABI cancellation exists during model creation.
211
+ lifetime.abort(failure('MAGPIE_LOADING_CANCELLED', 'The Magpie model load was cancelled.'));
212
+ } else {
213
+ await writeCommand([Buffer.from('close\t0\n')]);
214
+ input.end();
215
+ }
216
+ }
217
+ try { await completion; }
218
+ catch (error) {
219
+ if (!expectedCancellation(error, lifetime.signal)) throw error;
220
+ }
221
+ });
222
+ return closing;
223
+ }
224
+
225
+ return {
226
+ ready: readiness.promise, completion, synthesize, close,
227
+ get exited() { return exited; }
228
+ };
229
+ }
230
+
231
+ function deferred() {
232
+ const value = {};
233
+ value.promise = new Promise(function awaitResponse(resolve, reject) {
234
+ value.resolve = resolve;
235
+ value.reject = reject;
236
+ });
237
+ value.promise.catch(function observePendingFailure() {});
238
+ return value;
239
+ }
240
+
241
+ function failure(code, message, data) {
242
+ const error = new Error(message);
243
+ error.code = code;
244
+ if (data !== undefined) error.data = data;
245
+ return error;
246
+ }
247
+
248
+ function expectedCancellation(error, signal) {
249
+ if (!signal.aborted) return false;
250
+ if (error === signal.reason) return true;
251
+ if (!['ARCANE_CANCELLED', 'ABORT_ERR'].includes(error?.code) && error?.name !== 'AbortError') return false;
252
+ if (is.array(error.errors) && !error.errors.every(function cancelled(cause) { return expectedCancellation(cause, signal); })) return false;
253
+ return error.cause !== undefined && expectedCancellation(error.cause, signal);
254
+ }