arcane-os 0.92.0 → 0.94.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 (39) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/ai/twin-cloud.mjs +109 -46
  4. package/browser-runtime/core/client.mjs +20 -0
  5. package/browser-runtime/core/contracts.mjs +20 -0
  6. package/browser-runtime/core/native-diagnostics.mjs +217 -0
  7. package/docs/reference/ai/twin-cloud.md +35 -14
  8. package/docs/reference/availability-and-normalization.md +1 -1
  9. package/docs/reference/core/arcane-api.md +32 -0
  10. package/docs/reference/core/arcane-events.md +25 -0
  11. package/docs/reference/core-client.md +99 -0
  12. package/docs/reference/inventory/package-api.json +97 -13
  13. package/docs/reference/local-ai.md +81 -5
  14. package/docs/reference/native-diagnostics.md +167 -0
  15. package/docs/reference/native-speech.md +220 -11
  16. package/docs/reference/runtime-modules.md +21 -18
  17. package/docs/reference/sdk-api.md +152 -11
  18. package/package.json +4 -1
  19. package/schemas/arcane-app.schema.json +2 -2
  20. package/schemas/arcane-package.schema.json +2 -2
  21. package/src/core/app-control.mjs +1 -0
  22. package/src/core/hosts/windows/ArcaneAppControl.cs +1 -1
  23. package/src/core/hosts/windows/ArcaneHost.cs +167 -0
  24. package/src/core/hosts/windows/ArcaneLauncher.cs +1 -0
  25. package/src/core/hosts/windows/ArcaneNativeDiagnostics.cs +137 -0
  26. package/src/core/hosts/windows/ArcaneNotifications.cs +822 -0
  27. package/src/core/hosts/windows/ArcaneWindowControl.cs +3 -2
  28. package/src/core/services/speech.mjs +85 -23
  29. package/src/core-native-diagnostics-source.mjs +13 -0
  30. package/src/import-map.mjs +1 -0
  31. package/src/local-ai/config.mjs +12 -1
  32. package/src/local-ai/dev.mjs +71 -4
  33. package/src/local-ai/install.mjs +113 -5
  34. package/src/local-ai/kokoro/frontend.mjs +219 -0
  35. package/src/local-ai/kokoro/helper.mjs +168 -0
  36. package/src/local-ai/kokoro/index.mjs +645 -0
  37. package/src/local-ai/kokoro/native/CMakeLists.txt +45 -0
  38. package/src/local-ai/kokoro/native/main.cpp +249 -0
  39. 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
+ }
@@ -156,11 +156,18 @@ export function coreEntrySource(application, version, services, {localAI, runtim
156
156
  return (typeof requirement === 'string' ? requirement : requirement.id) === 'stable-diffusion.cpp';
157
157
  }) ?? false;
158
158
  const modelAssetsSelected = imageSelected || (localAI?.runtimes?.some(function selectedProjectedRuntime(requirement) {
159
- return ['onnx', 'llama.cpp'].includes(typeof requirement === 'string' ? requirement : requirement.id);
159
+ return ['onnx', 'llama.cpp', 'kokoro-native'].includes(typeof requirement === 'string' ? requirement : requirement.id);
160
160
  }) ?? false);
161
161
  const whisperSelected = localAI?.runtimes?.some(function selectedWhisper(requirement) {
162
162
  return (typeof requirement === 'string' ? requirement : requirement.id) === 'whisper.cpp';
163
163
  }) ?? false;
164
+ const kokoroRequirement = localAI?.runtimes?.find(function selectedKokoro(requirement) {
165
+ return (typeof requirement === 'string' ? requirement : requirement.id) === 'kokoro-native';
166
+ });
167
+ const kokoroSelected = kokoroRequirement !== undefined;
168
+ const onnxSelected = localAI?.runtimes?.some(function selectedONNX(requirement) {
169
+ return (typeof requirement === 'string' ? requirement : requirement.id) === 'onnx';
170
+ }) ?? false;
164
171
  const imports = services.map(function serviceImport(service, index) {
165
172
  const specifier = '../app/' + service.module.split('/').map(encodeURIComponent).join('/');
166
173
  return {binding: `{default: createService${index}}`, specifier};
@@ -174,7 +181,7 @@ export function coreEntrySource(application, version, services, {localAI, runtim
174
181
  ...localAI,
175
182
  runtimes: localAI.runtimes.filter(function selectedChat(requirement) {
176
183
  const id = typeof requirement === 'string' ? requirement : requirement.id;
177
- return id !== 'stable-diffusion.cpp' && id !== 'whisper.cpp';
184
+ return id !== 'stable-diffusion.cpp' && id !== 'whisper.cpp' && id !== 'kokoro-native';
178
185
  })
179
186
  };
180
187
  imports.push({binding: '{createLocalAIService}', specifier: 'arcane-os/core/local-ai'});
@@ -189,10 +196,15 @@ export function coreEntrySource(application, version, services, {localAI, runtim
189
196
  imports.push({binding: '{createLocalImageService}', specifier: 'arcane-os/core/image'});
190
197
  definitions.push(` register(createLocalImageService(JSON.parse(${JSON.stringify(JSON.stringify(localAI))}), {...context, modelAssets}));`);
191
198
  }
192
- if (whisperSelected) {
199
+ if (whisperSelected || kokoroSelected) {
193
200
  imports.push({binding: '{createSpeechService}', specifier: 'arcane-os/core/speech'});
201
+ }
202
+ if (whisperSelected) {
194
203
  imports.push({binding: '{createWhisperRuntime}', specifier: 'arcane-os/local-ai/whisper'});
195
204
  }
205
+ if (kokoroSelected) {
206
+ imports.push({binding: '{createNativeKokoroRuntime}', specifier: 'arcane-os/local-ai/kokoro'});
207
+ }
196
208
  if (packagedWeb) {
197
209
  imports.push({binding: '{createPackagedWebService}', specifier: 'arcane-os/core/packaged-web'});
198
210
  definitions.push(" packagedWeb = createPackagedWebService({artifactRoot: fileURLToPath(new URL('../', import.meta.url))}, context);",
@@ -228,7 +240,7 @@ export function coreEntrySource(application, version, services, {localAI, runtim
228
240
  ...(localAI === undefined ? [] : [
229
241
  ` runtimes: JSON.parse(${JSON.stringify(JSON.stringify(runtimes))}).map(function runtimeLocation(runtime) {`,
230
242
  ' const resolved = {...runtime};',
231
- " for (const field of ['root', 'executable', 'modulePath', 'includeDirectory', 'libraryDirectory', 'binaryDirectory', 'cmakeDirectory', 'libraryPath', 'bindingModulePath', 'helperExecutable', 'helperRoot', 'decoderExecutable', 'decoderRoot']) {",
243
+ " for (const field of ['root', 'executable', 'modulePath', 'includeDirectory', 'libraryDirectory', 'binaryDirectory', 'cmakeDirectory', 'libraryPath', 'bindingModulePath', 'helperExecutable', 'helperRoot', 'espeakDataDirectory', 'decoderExecutable', 'decoderRoot']) {",
232
244
  ' if (runtime[field] !== undefined) resolved[field] = runtimePath(runtime[field]);',
233
245
  ' }',
234
246
  ' if (runtime.variants) resolved.variants = runtime.variants.map(function nativeVariant(variant) {',
@@ -268,7 +280,7 @@ export function coreEntrySource(application, version, services, {localAI, runtim
268
280
  ''
269
281
  ]),
270
282
  ...(packagedWeb ? ['let packagedWeb;', ''] : []),
271
- 'async function createServices(register) {',
283
+ kokoroSelected ? 'async function createServices(register, getService) {' : 'async function createServices(register) {',
272
284
  ` const [${imports.map(function binding(selection) { return selection.binding; }).join(', ')}] = await Promise.all([`,
273
285
  ...imports.map(function importService(selection) { return ` import(${JSON.stringify(selection.specifier)})`; }).map(
274
286
  function separateImport(line, index) { return line + (index < imports.length - 1 ? ',' : ''); }
@@ -289,8 +301,50 @@ export function coreEntrySource(application, version, services, {localAI, runtim
289
301
  " temporaryDirectory: path.join(context.stateRoot ?? path.join(context.appRoot, '.arcane'), 'speech', 'whisper'),",
290
302
  ' onEvent: context.onEvent',
291
303
  ' }',
292
- ' );',
293
- ' register(createSpeechService({stt, signal: context.signal}));'
304
+ ' );'
305
+ ] : []),
306
+ ...(kokoroSelected ? [
307
+ ` const kokoroRequirement = JSON.parse(${JSON.stringify(JSON.stringify(typeof kokoroRequirement === 'string' ? {} : kokoroRequirement))});`,
308
+ ' const tts = createNativeKokoroRuntime({',
309
+ ' modelId: kokoroRequirement.modelId,',
310
+ ' model: kokoroRequirement.model,',
311
+ ' revision: kokoroRequirement.revision,',
312
+ ' dtype: kokoroRequirement.dtype,',
313
+ ' paths: kokoroRequirement.paths,',
314
+ ' assetProjectionId: kokoroRequirement.assetProjectionId,',
315
+ ' resourcePaths: kokoroRequirement.resourcePaths,',
316
+ ' sessionOptions: kokoroRequirement.sessionOptions,',
317
+ ' executionPreference: kokoroRequirement.executionPreference,',
318
+ ' executionTarget: kokoroRequirement.executionTarget,',
319
+ ' modelAssets, signal: context.signal, onEvent: context.onEvent,',
320
+ ' async prepare({signal: activationSignal}) {',
321
+ ' activationSignal.throwIfAborted();',
322
+ ...(onnxSelected ? [] : [
323
+ " throw Object.assign(new Error('Native Kokoro requires an explicitly selected onnx runtime.'), {code: 'ARCANE_TARGET_UNAVAILABLE'});"
324
+ ]),
325
+ ' let cancel;',
326
+ ' const cancelled = new Promise(function observePreparationCancellation(_resolve, reject) {',
327
+ ' cancel = function stopWaitingForONNX() { reject(activationSignal.reason); };',
328
+ " activationSignal.addEventListener('abort', cancel, {once: true});",
329
+ ' });',
330
+ ' try {',
331
+ ' // This activation waits on the existing owner without cancelling its startup.',
332
+ " const owner = await Promise.race([getService('local-ai'), cancelled]);",
333
+ ' activationSignal.throwIfAborted();',
334
+ ' return {',
335
+ ' onnx: owner.getONNXRuntime(),',
336
+ ' runtime: context.runtimes.find(function selectedKokoroRuntime(runtime) {',
337
+ " return runtime.id === 'kokoro-native';",
338
+ ' })',
339
+ ' };',
340
+ ' } finally {',
341
+ " activationSignal.removeEventListener('abort', cancel);",
342
+ ' }',
343
+ ' }',
344
+ ' });'
345
+ ] : []),
346
+ ...(whisperSelected || kokoroSelected ? [
347
+ ` register(createSpeechService({${whisperSelected ? 'stt, ' : ''}${kokoroSelected ? 'tts, ' : ''}signal: context.signal}));`
294
348
  ] : []),
295
349
  ...definitions,
296
350
  '}',
@@ -300,7 +354,9 @@ export function coreEntrySource(application, version, services, {localAI, runtim
300
354
  'let listener;',
301
355
  'if (context.sharedHost === undefined) {',
302
356
  ' const services = [];',
303
- ' await createServices(function register(service) { services.push(service); });',
357
+ kokoroSelected
358
+ ? ' await createServices(function register(service) { services.push(service); }, function getService(name) { return host.runtime.getService(name); });'
359
+ : ' await createServices(function register(service) { services.push(service); });',
304
360
  ' host = startCoreHost({application, version, services});',
305
361
  ' if (context.coreListener !== undefined) {',
306
362
  " process.on('SIGINT', closeCore);",
@@ -338,7 +394,9 @@ export function coreEntrySource(application, version, services, {localAI, runtim
338
394
  "} else if (process.argv.includes('--arcane-core-headless')) {",
339
395
  ' host = await runSharedCoreHost({',
340
396
  ' endpoint: context.sharedHost.endpoint, application, version,',
341
- ' configure(runtime) { return createServices(function register(service) { runtime.registerService(service); }); },',
397
+ kokoroSelected
398
+ ? ' configure(runtime) { return createServices(function register(service) { runtime.registerService(service); }, function getService(name) { return runtime.getService(name); }); },'
399
+ : ' configure(runtime) { return createServices(function register(service) { runtime.registerService(service); }); },',
342
400
  ...(packagedWeb ? [
343
401
  ' getReplayEvents() {',
344
402
  ' const current = packagedWeb?.current();',