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.
- package/CHANGELOG.md +33 -0
- package/README.md +1 -1
- package/browser-runtime/ai/twin-cloud.mjs +109 -46
- package/browser-runtime/core/client.mjs +197 -0
- package/docs/reference/ai/twin-cloud.md +35 -14
- package/docs/reference/availability-and-normalization.md +1 -1
- package/docs/reference/inventory/package-api.json +96 -12
- package/docs/reference/local-ai.md +81 -5
- package/docs/reference/native-magpie.md +98 -0
- package/docs/reference/native-nemotron.md +204 -0
- package/docs/reference/native-speech.md +364 -11
- package/docs/reference/runtime-modules.md +21 -18
- package/docs/reference/sdk-api.md +152 -11
- package/package.json +6 -1
- package/runtime/arcane/modules/AI.js +114 -14
- package/runtime/arcane/modules/AIProviderRuntime.js +32 -15
- package/runtime/arcane/modules/ContinuousVoiceCapture.js +118 -21
- package/runtime/arcane/modules/NativeSpeechCapture.js +198 -0
- package/runtime/arcane/modules/NativeSpeechPlayback.js +304 -0
- package/runtime/arcane/modules/VoiceCaptureWorklet.js +62 -2
- package/schemas/arcane-app.schema.json +2 -2
- package/schemas/arcane-package.schema.json +2 -2
- package/src/core/services/speech.mjs +320 -25
- package/src/local-ai/config.mjs +12 -1
- package/src/local-ai/dev.mjs +71 -4
- package/src/local-ai/install.mjs +113 -5
- package/src/local-ai/kokoro/frontend.mjs +219 -0
- package/src/local-ai/kokoro/helper.mjs +168 -0
- package/src/local-ai/kokoro/index.mjs +645 -0
- package/src/local-ai/kokoro/native/CMakeLists.txt +45 -0
- package/src/local-ai/kokoro/native/main.cpp +249 -0
- package/src/local-ai/magpie/build.mjs +36 -0
- package/src/local-ai/magpie/helper.mjs +254 -0
- package/src/local-ai/magpie/index.mjs +488 -0
- package/src/local-ai/magpie/native/CMakeLists.txt +10 -0
- package/src/local-ai/magpie/native/main.cpp +270 -0
- package/src/local-ai/nemotron/build.mjs +53 -0
- package/src/local-ai/nemotron/helper.mjs +308 -0
- package/src/local-ai/nemotron/index.mjs +637 -0
- package/src/local-ai/nemotron/native/CMakeLists.txt +17 -0
- package/src/local-ai/nemotron/native/main.cpp +282 -0
- package/src/native/portable-layout.mjs +67 -9
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#include <nemo_speech/asr.h>
|
|
2
|
+
|
|
3
|
+
#include <cstdint>
|
|
4
|
+
#include <cstring>
|
|
5
|
+
#include <iomanip>
|
|
6
|
+
#include <iostream>
|
|
7
|
+
#include <limits>
|
|
8
|
+
#include <locale>
|
|
9
|
+
#include <memory>
|
|
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
|
+
namespace {
|
|
22
|
+
|
|
23
|
+
using Recognizer = std::unique_ptr<nemo_speech_asr_recognizer, decltype(&nemo_speech_asr_destroy)>;
|
|
24
|
+
using Stream = std::unique_ptr<nemo_speech_asr_stream, decltype(&nemo_speech_asr_stream_close)>;
|
|
25
|
+
using Result = std::unique_ptr<nemo_speech_asr_result, decltype(&nemo_speech_asr_result_destroy)>;
|
|
26
|
+
|
|
27
|
+
void check(nemo_speech_asr_status status) {
|
|
28
|
+
if (status != NEMO_SPEECH_ASR_OK) {
|
|
29
|
+
const char* message = nemo_speech_asr_last_error();
|
|
30
|
+
throw std::runtime_error(message ? message : "The native ASR operation failed.");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
std::string json_string(const char* value) {
|
|
35
|
+
std::ostringstream output;
|
|
36
|
+
output << '"';
|
|
37
|
+
if (value) {
|
|
38
|
+
for (const unsigned char character : std::string(value)) {
|
|
39
|
+
switch (character) {
|
|
40
|
+
case '"': output << "\\\""; break;
|
|
41
|
+
case '\\': output << "\\\\"; break;
|
|
42
|
+
case '\n': output << "\\n"; break;
|
|
43
|
+
case '\r': output << "\\r"; break;
|
|
44
|
+
case '\t': output << "\\t"; break;
|
|
45
|
+
default:
|
|
46
|
+
if (character < 0x20) {
|
|
47
|
+
output << "\\u" << std::hex << std::setw(4) << std::setfill('0')
|
|
48
|
+
<< static_cast<unsigned int>(character) << std::dec;
|
|
49
|
+
} else {
|
|
50
|
+
output << character;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
output << '"';
|
|
56
|
+
return output.str();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
void emit_error(uint64_t request, const std::string& message) {
|
|
60
|
+
std::cout << "{\"request\":" << request << ",\"error\":" << json_string(message.c_str())
|
|
61
|
+
<< "}\n" << std::flush;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
void emit_ack(uint64_t request) {
|
|
65
|
+
std::cout << "{\"request\":" << request << ",\"ok\":true}\n" << std::flush;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
void emit_running(uint64_t request) {
|
|
69
|
+
std::cout << "{\"request\":" << request << ",\"running\":true}\n" << std::flush;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
std::string read_text(size_t count) {
|
|
73
|
+
std::string text(count, '\0');
|
|
74
|
+
if (count && !std::cin.read(text.data(), count)) {
|
|
75
|
+
throw std::runtime_error("Nemotron input ended during a language field.");
|
|
76
|
+
}
|
|
77
|
+
return text;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
std::vector<float> read_audio(size_t samples) {
|
|
81
|
+
std::vector<float> audio(samples);
|
|
82
|
+
std::vector<unsigned char> encoded(samples * sizeof(float));
|
|
83
|
+
if (!encoded.empty() && !std::cin.read(reinterpret_cast<char*>(encoded.data()), encoded.size())) {
|
|
84
|
+
throw std::runtime_error("Nemotron input ended during a float32 audio frame.");
|
|
85
|
+
}
|
|
86
|
+
for (size_t index = 0; index < samples; ++index) {
|
|
87
|
+
const auto* value = encoded.data() + index * sizeof(float);
|
|
88
|
+
const uint32_t bits = static_cast<uint32_t>(value[0])
|
|
89
|
+
| (static_cast<uint32_t>(value[1]) << 8)
|
|
90
|
+
| (static_cast<uint32_t>(value[2]) << 16)
|
|
91
|
+
| (static_cast<uint32_t>(value[3]) << 24);
|
|
92
|
+
std::memcpy(&audio[index], &bits, sizeof(float));
|
|
93
|
+
}
|
|
94
|
+
return audio;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
void emit_result(uint64_t request, uint64_t sequence, const nemo_speech_asr_result* result) {
|
|
98
|
+
std::ostringstream output;
|
|
99
|
+
output.imbue(std::locale::classic());
|
|
100
|
+
output << std::setprecision(std::numeric_limits<float>::max_digits10);
|
|
101
|
+
output << "{\"request\":" << request << ",\"update\":{\"sequence\":" << sequence
|
|
102
|
+
<< ",\"final\":" << (nemo_speech_asr_result_is_final(result) ? "true" : "false")
|
|
103
|
+
<< ",\"audioProcessedSeconds\":" << nemo_speech_asr_result_audio_processed(result)
|
|
104
|
+
<< ",\"channelTag\":" << nemo_speech_asr_result_channel_tag(result)
|
|
105
|
+
<< ",\"alternatives\":[";
|
|
106
|
+
const size_t alternatives = nemo_speech_asr_result_alternative_count(result);
|
|
107
|
+
for (size_t alternative = 0; alternative < alternatives; ++alternative) {
|
|
108
|
+
if (alternative) output << ',';
|
|
109
|
+
output << "{\"text\":" << json_string(nemo_speech_asr_result_transcript(result, alternative))
|
|
110
|
+
<< ",\"confidence\":" << nemo_speech_asr_result_confidence(result, alternative)
|
|
111
|
+
<< ",\"words\":[";
|
|
112
|
+
const size_t words = nemo_speech_asr_result_word_count(result, alternative);
|
|
113
|
+
for (size_t index = 0; index < words; ++index) {
|
|
114
|
+
if (index) output << ',';
|
|
115
|
+
output << "{\"word\":" << json_string(nemo_speech_asr_result_word_text(result, alternative, index))
|
|
116
|
+
<< ",\"startTimeMs\":" << nemo_speech_asr_result_word_start_time(result, alternative, index)
|
|
117
|
+
<< ",\"endTimeMs\":" << nemo_speech_asr_result_word_end_time(result, alternative, index)
|
|
118
|
+
<< ",\"confidence\":" << nemo_speech_asr_result_word_confidence(result, alternative, index)
|
|
119
|
+
<< ",\"speaker\":" << nemo_speech_asr_result_word_speaker_tag(result, alternative, index) << '}';
|
|
120
|
+
}
|
|
121
|
+
output << "],\"languageCodes\":[";
|
|
122
|
+
const size_t languages = nemo_speech_asr_result_language_count(result, alternative);
|
|
123
|
+
for (size_t index = 0; index < languages; ++index) {
|
|
124
|
+
if (index) output << ',';
|
|
125
|
+
output << json_string(nemo_speech_asr_result_language_code(result, alternative, index));
|
|
126
|
+
}
|
|
127
|
+
output << "]}";
|
|
128
|
+
}
|
|
129
|
+
output << "]}}\n";
|
|
130
|
+
// A complete result is emitted before asking for the next one. An error
|
|
131
|
+
// later in the drain cannot erase earlier native results.
|
|
132
|
+
std::cout << output.str() << std::flush;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
void drain(uint64_t request, nemo_speech_asr_stream* stream, uint64_t& sequence) {
|
|
136
|
+
while (true) {
|
|
137
|
+
nemo_speech_asr_result* raw = nullptr;
|
|
138
|
+
const auto status = nemo_speech_asr_stream_next(stream, &raw);
|
|
139
|
+
const Result result(raw, nemo_speech_asr_result_destroy);
|
|
140
|
+
check(status);
|
|
141
|
+
if (!result) return;
|
|
142
|
+
emit_result(request, ++sequence, result.get());
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
int serve(const std::string& model_path, const std::string& diarization_path, int gpu) {
|
|
147
|
+
#ifdef _WIN32
|
|
148
|
+
_setmode(_fileno(stdin), _O_BINARY);
|
|
149
|
+
_setmode(_fileno(stdout), _O_BINARY);
|
|
150
|
+
#endif
|
|
151
|
+
nemo_speech_asr_backend_config backend{};
|
|
152
|
+
backend.size = sizeof(backend);
|
|
153
|
+
backend.gpu = gpu;
|
|
154
|
+
nemo_speech_asr_model_config model{};
|
|
155
|
+
model.size = sizeof(model);
|
|
156
|
+
model.path = model_path.c_str();
|
|
157
|
+
nemo_speech_asr_diar_config diarization{};
|
|
158
|
+
diarization.size = sizeof(diarization);
|
|
159
|
+
diarization.model_path = diarization_path.empty() ? nullptr : diarization_path.c_str();
|
|
160
|
+
diarization.left_context_frames = -1;
|
|
161
|
+
nemo_speech_asr_endpointing_config endpointing{};
|
|
162
|
+
endpointing.size = sizeof(endpointing);
|
|
163
|
+
endpointing.enable = false;
|
|
164
|
+
nemo_speech_asr_recognizer_config configuration{};
|
|
165
|
+
configuration.size = sizeof(configuration);
|
|
166
|
+
configuration.backend = &backend;
|
|
167
|
+
configuration.model = &model;
|
|
168
|
+
configuration.diar = &diarization;
|
|
169
|
+
configuration.endpointing = &endpointing;
|
|
170
|
+
nemo_speech_asr_recognizer* raw_recognizer = nullptr;
|
|
171
|
+
const auto status = nemo_speech_asr_create(&configuration, &raw_recognizer);
|
|
172
|
+
const Recognizer recognizer(raw_recognizer, nemo_speech_asr_destroy);
|
|
173
|
+
check(status);
|
|
174
|
+
// The stream is destroyed before its recognizer. All stream calls stay on
|
|
175
|
+
// this thread; the host owns capture, timing and conversational decisions.
|
|
176
|
+
Stream stream(nullptr, nemo_speech_asr_stream_close);
|
|
177
|
+
int sample_rate = 16000;
|
|
178
|
+
uint64_t sequence = 0;
|
|
179
|
+
std::string language;
|
|
180
|
+
std::cout << "{\"ready\":true,\"version\":" << json_string(nemo_speech_asr_version())
|
|
181
|
+
<< ",\"requestedGpu\":" << gpu << "}\n" << std::flush;
|
|
182
|
+
std::string line;
|
|
183
|
+
while (std::getline(std::cin, line)) {
|
|
184
|
+
uint64_t request = 0;
|
|
185
|
+
std::string operation;
|
|
186
|
+
std::istringstream command(line);
|
|
187
|
+
if (!(command >> operation >> request)) {
|
|
188
|
+
emit_error(0, "Unreadable Nemotron command.");
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
if (operation == "open") {
|
|
193
|
+
size_t language_length = 0;
|
|
194
|
+
int requested_rate = 0;
|
|
195
|
+
if (!(command >> requested_rate >> language_length)) {
|
|
196
|
+
throw std::runtime_error("Unreadable Nemotron stream configuration.");
|
|
197
|
+
}
|
|
198
|
+
const std::string requested_language = read_text(language_length);
|
|
199
|
+
if (stream) throw std::runtime_error("A Nemotron stream is already open.");
|
|
200
|
+
language = requested_language;
|
|
201
|
+
sample_rate = requested_rate;
|
|
202
|
+
auto options = nemo_speech_asr_recognition_options_default();
|
|
203
|
+
options.language_code = language.empty() ? nullptr : language.c_str();
|
|
204
|
+
options.interim_results = true;
|
|
205
|
+
options.enable_word_time_offsets = true;
|
|
206
|
+
options.enable_speaker_diarization = !diarization_path.empty();
|
|
207
|
+
options.enable_automatic_punctuation = false;
|
|
208
|
+
options.profanity_filter = false;
|
|
209
|
+
options.verbatim_transcripts = true;
|
|
210
|
+
nemo_speech_asr_stream* raw_stream = nullptr;
|
|
211
|
+
emit_running(request);
|
|
212
|
+
const auto opened = nemo_speech_asr_streaming_recognize(recognizer.get(), &options, &raw_stream);
|
|
213
|
+
stream.reset(raw_stream);
|
|
214
|
+
check(opened);
|
|
215
|
+
sequence = 0;
|
|
216
|
+
} else if (operation == "close") {
|
|
217
|
+
stream.reset();
|
|
218
|
+
} else {
|
|
219
|
+
std::vector<float> audio;
|
|
220
|
+
if (operation == "push") {
|
|
221
|
+
size_t samples = 0;
|
|
222
|
+
if (!(command >> samples)) throw std::runtime_error("Unreadable Nemotron audio frame.");
|
|
223
|
+
audio = read_audio(samples);
|
|
224
|
+
}
|
|
225
|
+
if (!stream) throw std::runtime_error("No Nemotron stream is open.");
|
|
226
|
+
emit_running(request);
|
|
227
|
+
if (operation == "push") {
|
|
228
|
+
check(nemo_speech_asr_stream_push_f32(stream.get(), audio.data(), audio.size(), sample_rate));
|
|
229
|
+
} else if (operation == "endpoint") {
|
|
230
|
+
check(nemo_speech_asr_stream_force_endpoint(stream.get()));
|
|
231
|
+
} else if (operation == "finish") {
|
|
232
|
+
check(nemo_speech_asr_stream_finish(stream.get()));
|
|
233
|
+
} else {
|
|
234
|
+
throw std::runtime_error("Unknown Nemotron operation.");
|
|
235
|
+
}
|
|
236
|
+
drain(request, stream.get(), sequence);
|
|
237
|
+
if (operation == "finish") stream.reset();
|
|
238
|
+
}
|
|
239
|
+
emit_ack(request);
|
|
240
|
+
} catch (const std::exception& error) {
|
|
241
|
+
emit_error(request, error.what());
|
|
242
|
+
if (!std::cin) return 1;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
int run(const std::vector<std::string>& arguments) {
|
|
249
|
+
try {
|
|
250
|
+
if (arguments.size() != 4) {
|
|
251
|
+
throw std::runtime_error("Usage: arcane-nemotron ASR.gguf DIARIZATION.gguf GPU_ORDINAL (-1 selects CPU).");
|
|
252
|
+
}
|
|
253
|
+
return serve(arguments[1], arguments[2], std::stoi(arguments[3]));
|
|
254
|
+
} catch (const std::exception& error) {
|
|
255
|
+
emit_error(0, error.what());
|
|
256
|
+
return 1;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
} // namespace
|
|
261
|
+
|
|
262
|
+
#ifdef _WIN32
|
|
263
|
+
int wmain(int argc, wchar_t** argv) {
|
|
264
|
+
std::vector<std::string> arguments;
|
|
265
|
+
for (int index = 0; index < argc; ++index) {
|
|
266
|
+
const int length = WideCharToMultiByte(CP_UTF8, 0, argv[index], -1, nullptr, 0, nullptr, nullptr);
|
|
267
|
+
if (length == 0) {
|
|
268
|
+
emit_error(0, "Windows could not encode a Nemotron argument as UTF-8.");
|
|
269
|
+
return 1;
|
|
270
|
+
}
|
|
271
|
+
std::string argument(length, '\0');
|
|
272
|
+
WideCharToMultiByte(CP_UTF8, 0, argv[index], -1, argument.data(), length, nullptr, nullptr);
|
|
273
|
+
argument.pop_back();
|
|
274
|
+
arguments.push_back(argument);
|
|
275
|
+
}
|
|
276
|
+
return run(arguments);
|
|
277
|
+
}
|
|
278
|
+
#else
|
|
279
|
+
int main(int argc, char** argv) {
|
|
280
|
+
return run(std::vector<std::string>(argv, argv + argc));
|
|
281
|
+
}
|
|
282
|
+
#endif
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();',
|