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,270 @@
1
+ #include <nemo_speech/tts.h>
2
+
3
+ #include <atomic>
4
+ #include <condition_variable>
5
+ #include <cstdint>
6
+ #include <exception>
7
+ #include <iostream>
8
+ #include <memory>
9
+ #include <mutex>
10
+ #include <sstream>
11
+ #include <stdexcept>
12
+ #include <string>
13
+ #include <thread>
14
+ #include <utility>
15
+ #include <vector>
16
+
17
+ #ifdef _WIN32
18
+ #include <fcntl.h>
19
+ #include <io.h>
20
+ #include <windows.h>
21
+ #endif
22
+
23
+ namespace {
24
+
25
+ using Synthesizer = std::unique_ptr<nemo_speech_tts_synthesizer, decltype(&nemo_speech_tts_destroy)>;
26
+
27
+ struct Utterance {
28
+ uint64_t id;
29
+ int32_t speaker;
30
+ std::string language;
31
+ std::string text;
32
+ std::atomic<bool> cancelled{false};
33
+ };
34
+
35
+ std::mutex output_mutex;
36
+
37
+ // Lengths belong only to this binary transport. Every PCM and text frame is complete.
38
+ void emit(const char* kind, uint64_t id, const char* content, size_t length) {
39
+ const std::lock_guard<std::mutex> lock(output_mutex);
40
+ std::cout << kind << '\t' << id << '\t' << length << '\n';
41
+ std::cout.write(content, static_cast<std::streamsize>(length));
42
+ std::cout.flush();
43
+ if (!std::cout) throw std::runtime_error("The Magpie output pipe closed.");
44
+ }
45
+
46
+ void emit(const char* kind, uint64_t id, const std::string& content = {}) {
47
+ emit(kind, id, content.data(), content.size());
48
+ }
49
+
50
+ struct Output {
51
+ Utterance& utterance;
52
+ std::string error;
53
+ };
54
+
55
+ bool deliver_audio(const uint8_t* pcm, size_t length, void* context) {
56
+ auto& output = *static_cast<Output*>(context);
57
+ if (output.utterance.cancelled.load()) return false;
58
+ try {
59
+ emit("audio", output.utterance.id, reinterpret_cast<const char*>(pcm), length);
60
+ return !output.utterance.cancelled.load();
61
+ } catch (const std::exception& error) {
62
+ output.error = error.what();
63
+ return false;
64
+ }
65
+ }
66
+
67
+ std::string read_text(size_t length) {
68
+ std::string text(length, '\0');
69
+ if (!std::cin.read(text.data(), static_cast<std::streamsize>(length))) {
70
+ throw std::runtime_error("The Magpie input pipe ended during a text frame.");
71
+ }
72
+ return text;
73
+ }
74
+
75
+ int serve(const std::vector<std::string>& arguments) {
76
+ #ifdef _WIN32
77
+ _setmode(_fileno(stdin), _O_BINARY);
78
+ _setmode(_fileno(stdout), _O_BINARY);
79
+ #endif
80
+ if (arguments.size() != 6) {
81
+ throw std::runtime_error("Usage: arcane-magpie MAGPIE.gguf CODEC.gguf TOKENIZER_DIR SPEAKER cpu|cuda");
82
+ }
83
+ const std::string& backend = arguments[5];
84
+ if (backend != "cpu" && backend != "cuda") {
85
+ throw std::runtime_error("Select the prepared CPU or CUDA TTS backend explicitly.");
86
+ }
87
+ nemo_speech_tts_model_config models = {};
88
+ models.size = sizeof(models);
89
+ models.magpie_model = arguments[1].c_str();
90
+ models.codec_model = arguments[2].c_str();
91
+ models.tokenizer_model_dir = arguments[3].c_str();
92
+ // Leave optional written-to-spoken normalization absent: the caller owns its text.
93
+ auto runtime = nemo_speech_tts_runtime_config_default();
94
+ runtime.speaker = std::stoi(arguments[4]);
95
+ runtime.lt_backend = backend == "cuda" ? NEMO_SPEECH_TTS_BACKEND_CUDA : NEMO_SPEECH_TTS_BACKEND_CPU;
96
+ runtime.sampling_backend = runtime.lt_backend;
97
+ runtime.codec_cpu = backend == "cpu";
98
+ nemo_speech_tts_synthesizer_config configuration = {};
99
+ configuration.size = sizeof(configuration);
100
+ configuration.model = &models;
101
+ configuration.runtime = &runtime;
102
+ nemo_speech_tts_synthesizer* raw = nullptr;
103
+ if (nemo_speech_tts_create(&configuration, &raw) != NEMO_SPEECH_TTS_OK) {
104
+ const char* error = nemo_speech_tts_last_error();
105
+ throw std::runtime_error(error && *error ? error : "The native TTS model creation failed without an error message.");
106
+ }
107
+ const Synthesizer synthesizer(raw, nemo_speech_tts_destroy);
108
+ emit("ready", 0, std::to_string(nemo_speech_tts_sample_rate(synthesizer.get())));
109
+
110
+ std::mutex work_mutex;
111
+ std::condition_variable work_ready;
112
+ std::shared_ptr<Utterance> pending;
113
+ std::shared_ptr<Utterance> active;
114
+ bool closing = false;
115
+ std::exception_ptr worker_error;
116
+
117
+ // Only this worker calls the synthesizer. The reader can flag the exact
118
+ // active utterance while inference or PCM backpressure occupies that worker.
119
+ std::thread worker([&]() {
120
+ try {
121
+ for (;;) {
122
+ std::shared_ptr<Utterance> utterance;
123
+ {
124
+ std::unique_lock<std::mutex> lock(work_mutex);
125
+ work_ready.wait(lock, [&]() { return closing || pending != nullptr; });
126
+ if (!pending) return;
127
+ utterance = std::move(pending);
128
+ active = utterance;
129
+ }
130
+ auto options = nemo_speech_tts_synthesis_options_default();
131
+ const std::string request_id = std::to_string(utterance->id);
132
+ options.request_id = request_id.c_str();
133
+ options.language_code = utterance->language.c_str();
134
+ options.speaker = utterance->speaker;
135
+ Output output{*utterance, {}};
136
+ nemo_speech_tts_status status = NEMO_SPEECH_TTS_ERROR_CANCELLED;
137
+ if (!utterance->cancelled.load()) {
138
+ emit("started", utterance->id);
139
+ status = nemo_speech_tts_synthesize_text(
140
+ synthesizer.get(), &options, utterance->text.c_str(), deliver_audio, &output, nullptr
141
+ );
142
+ }
143
+ // Read the thread-local native error before any other native call.
144
+ std::string native_error;
145
+ if (status != NEMO_SPEECH_TTS_OK && status != NEMO_SPEECH_TTS_ERROR_CANCELLED) {
146
+ const char* message = nemo_speech_tts_last_error();
147
+ native_error = message && *message ? message
148
+ : "The native TTS call failed with status " + std::to_string(status) + ".";
149
+ }
150
+ {
151
+ const std::lock_guard<std::mutex> lock(work_mutex);
152
+ active.reset();
153
+ }
154
+ if (!output.error.empty()) emit("error", utterance->id, output.error);
155
+ else if (!native_error.empty()) emit("error", utterance->id, native_error);
156
+ else if (utterance->cancelled.load() || status == NEMO_SPEECH_TTS_ERROR_CANCELLED) {
157
+ emit("cancelled", utterance->id);
158
+ } else emit("done", utterance->id);
159
+ }
160
+ } catch (...) {
161
+ const auto failure = std::current_exception();
162
+ {
163
+ const std::lock_guard<std::mutex> lock(work_mutex);
164
+ worker_error = failure;
165
+ closing = true;
166
+ }
167
+ // A failed worker must wake the process owner, which closes input
168
+ // and joins this process instead of leaving an unanswered request.
169
+ try {
170
+ try { std::rethrow_exception(failure); }
171
+ catch (const std::exception& error) { emit("fatal", 0, error.what()); }
172
+ catch (...) { emit("fatal", 0, "The native synthesis worker raised an unknown exception."); }
173
+ } catch (...) {
174
+ // A closed output pipe cannot carry another diagnostic.
175
+ }
176
+ }
177
+ });
178
+
179
+ std::exception_ptr reader_error;
180
+ try {
181
+ std::string line;
182
+ while (std::getline(std::cin, line)) {
183
+ std::istringstream command(line);
184
+ std::string operation;
185
+ uint64_t id = 0;
186
+ if (!(command >> operation >> id)) throw std::runtime_error("Unreadable Magpie command.");
187
+ if (operation == "synthesize") {
188
+ int32_t speaker = 0;
189
+ size_t language_length = 0;
190
+ size_t text_length = 0;
191
+ if (!(command >> speaker >> language_length >> text_length)) {
192
+ throw std::runtime_error("Unreadable Magpie synthesis framing.");
193
+ }
194
+ auto utterance = std::make_shared<Utterance>();
195
+ utterance->id = id;
196
+ utterance->speaker = speaker;
197
+ utterance->language = read_text(language_length);
198
+ utterance->text = read_text(text_length);
199
+ if (utterance->language.find('\0') != std::string::npos || utterance->text.find('\0') != std::string::npos) {
200
+ emit("error", id, "The public TTS text interface cannot consume embedded NUL characters.");
201
+ continue;
202
+ }
203
+ {
204
+ const std::lock_guard<std::mutex> lock(work_mutex);
205
+ if (worker_error) std::rethrow_exception(worker_error);
206
+ if (closing || pending || active) {
207
+ emit("error", id, "The native synthesizer already owns an utterance.");
208
+ continue;
209
+ }
210
+ pending = utterance;
211
+ }
212
+ work_ready.notify_one();
213
+ } else if (operation == "stop") {
214
+ const std::lock_guard<std::mutex> lock(work_mutex);
215
+ if (pending && pending->id == id) pending->cancelled.store(true);
216
+ if (active && active->id == id) active->cancelled.store(true);
217
+ } else if (operation == "close") {
218
+ break;
219
+ } else {
220
+ emit("error", id, "Unknown Magpie command.");
221
+ }
222
+ }
223
+ } catch (...) {
224
+ reader_error = std::current_exception();
225
+ }
226
+ {
227
+ const std::lock_guard<std::mutex> lock(work_mutex);
228
+ closing = true;
229
+ if (pending) pending->cancelled.store(true);
230
+ if (active) active->cancelled.store(true);
231
+ }
232
+ work_ready.notify_one();
233
+ worker.join();
234
+ if (reader_error) std::rethrow_exception(reader_error);
235
+ if (worker_error) std::rethrow_exception(worker_error);
236
+ return 0;
237
+ }
238
+
239
+ int run(const std::vector<std::string>& arguments) {
240
+ try {
241
+ return serve(arguments);
242
+ } catch (const std::exception& error) {
243
+ std::cerr << error.what() << '\n';
244
+ return 1;
245
+ }
246
+ }
247
+
248
+ } // namespace
249
+
250
+ #ifdef _WIN32
251
+ int wmain(int argc, wchar_t** argv) {
252
+ std::vector<std::string> arguments;
253
+ for (int index = 0; index < argc; ++index) {
254
+ const int length = WideCharToMultiByte(CP_UTF8, 0, argv[index], -1, nullptr, 0, nullptr, nullptr);
255
+ if (length == 0) {
256
+ std::cerr << "Windows could not encode the selected TTS argument as UTF-8.\n";
257
+ return 1;
258
+ }
259
+ std::string value(length, '\0');
260
+ WideCharToMultiByte(CP_UTF8, 0, argv[index], -1, value.data(), length, nullptr, nullptr);
261
+ value.pop_back();
262
+ arguments.push_back(std::move(value));
263
+ }
264
+ return run(arguments);
265
+ }
266
+ #else
267
+ int main(int argc, char** argv) {
268
+ return run(std::vector<std::string>(argv, argv + argc));
269
+ }
270
+ #endif
@@ -0,0 +1,160 @@
1
+ import {randomUUID} from 'node:crypto';
2
+
3
+ /** Selected native assets from NVIDIA NeMo-Speech.cpp v0.2.0 models/index.json. */
4
+ export const NEMO_SPEECH_MODELS = {
5
+ stt: {
6
+ modelId: 'nemotron-3.5-asr-streaming-0.6b',
7
+ providerId: 'nemotron',
8
+ name: 'Nemotron 3.5 + speaker diarization',
9
+ members: [
10
+ {
11
+ path: 'nemotron-3.5-asr-streaming-0.6b.q8_0.gguf',
12
+ url: 'https://huggingface.co/nvidia/nemotron-3.5-asr-streaming-0.6b/resolve/1c8deaecc64b91f034d73e08dd8b64625eb3395d/nemotron-3.5-asr-streaming-0.6b.q8_0.gguf'
13
+ },
14
+ {
15
+ path: 'Nemotron-3-Diarization.q8_0.gguf',
16
+ url: 'https://huggingface.co/nvidia/Nemotron-3-Diarization/resolve/f667ed73aee57d40cc39428eb768b4fd87a0a29e/Nemotron-3-Diarization.q8_0.gguf'
17
+ }
18
+ ],
19
+ resourcePaths: {
20
+ model: 'nemotron-3.5-asr-streaming-0.6b.q8_0.gguf',
21
+ diarization: 'Nemotron-3-Diarization.q8_0.gguf'
22
+ }
23
+ },
24
+ tts: {
25
+ modelId: 'magpie',
26
+ providerId: 'magpie-native',
27
+ name: 'Magpie + NanoCodec',
28
+ members: [
29
+ {
30
+ path: 'magpie_tts_multilingual_357m.v2607.f16.gguf',
31
+ url: 'https://huggingface.co/nvidia/magpie_tts_multilingual_357m/resolve/19806879b16d3f2ccf28fb112b1bcd16a3c7923e/magpie_tts_multilingual_357m.v2607.f16.gguf'
32
+ },
33
+ {
34
+ path: 'nemo_nano_codec_22khz_1.89kbps_21.5fps.decoder.f16.gguf',
35
+ url: 'https://huggingface.co/nvidia/nemo-nano-codec-22khz-1.89kbps-21.5fps/resolve/fc00890b604aa2de298d2641ffc6c5f6caf8c4d7/nemo_nano_codec_22khz_1.89kbps_21.5fps.decoder.f16.gguf'
36
+ }
37
+ ],
38
+ archives: [
39
+ {
40
+ url: 'https://huggingface.co/nvidia/magpie_tts_multilingual_357m/resolve/19806879b16d3f2ccf28fb112b1bcd16a3c7923e/magpie_tts_multilingual_357m.nemo',
41
+ format: 'tar',
42
+ // These offsets select upstream TAR segments at the transport
43
+ // boundary. Their complete selected members stay unchanged.
44
+ ranges: [
45
+ {start: 0, end: 20619263, stopBefore: 'model_weights.ckpt'},
46
+ {start: 1459750400, end: 1470207999}
47
+ ],
48
+ members: [
49
+ {
50
+ path: 'tokenizer/05adc40366e149b69319acd4b28a4919_pt_br_prondict-v1.0.dict',
51
+ member: '05adc40366e149b69319acd4b28a4919_pt_br_prondict-v1.0.dict'
52
+ },
53
+ {
54
+ path: 'tokenizer/339da71c54b046f98cbcf38ef6d4ff67_hindi_phoneme_merged_phoneme_dict.dict',
55
+ member: '339da71c54b046f98cbcf38ef6d4ff67_hindi_phoneme_merged_phoneme_dict.dict'
56
+ },
57
+ {
58
+ path: 'tokenizer/41913ebaa70342058574da74293b7630_magpie_tts_multilingual_357m.nemo.speakers.json',
59
+ member: '41913ebaa70342058574da74293b7630_magpie_tts_multilingual_357m.nemo.speakers.json'
60
+ },
61
+ {
62
+ path: 'tokenizer/61d57a4ccb064d1e8e3d9f871c571932_heteronyms-052722',
63
+ member: '61d57a4ccb064d1e8e3d9f871c571932_heteronyms-052722'
64
+ },
65
+ {
66
+ path: 'tokenizer/74b3e832fe914a569fcd42b51d06b2f1_ipa_dict_nv23.05.txt',
67
+ member: '74b3e832fe914a569fcd42b51d06b2f1_ipa_dict_nv23.05.txt'
68
+ },
69
+ {
70
+ path: 'tokenizer/7dbc31751f224f2486090d59dc95b9f7_es_ES_nv230301.dict',
71
+ member: '7dbc31751f224f2486090d59dc95b9f7_es_ES_nv230301.dict'
72
+ },
73
+ {
74
+ path: 'tokenizer/c5e4ec2af5a14ce294f4b9edcc936535_de_nv230119.heteronym',
75
+ member: 'c5e4ec2af5a14ce294f4b9edcc936535_de_nv230119.heteronym'
76
+ },
77
+ {
78
+ path: 'tokenizer/cf01ab5c48c84f3282ef7888263361e5_de_nv230119.dict',
79
+ member: 'cf01ab5c48c84f3282ef7888263361e5_de_nv230119.dict'
80
+ },
81
+ {
82
+ path: 'tokenizer/dc7d60d6b15a4651b21c9ca2932b62c6_ipa_cmudict-0.7b_nv23.01.txt',
83
+ member: 'dc7d60d6b15a4651b21c9ca2932b62c6_ipa_cmudict-0.7b_nv23.01.txt'
84
+ },
85
+ {path: 'tokenizer/model_config.yaml', member: 'model_config.yaml'}
86
+ ]
87
+ }
88
+ ],
89
+ resourcePaths: {
90
+ model: 'magpie_tts_multilingual_357m.v2607.f16.gguf',
91
+ codec: 'nemo_nano_codec_22khz_1.89kbps_21.5fps.decoder.f16.gguf',
92
+ tokenizerConfig: 'tokenizer/model_config.yaml'
93
+ }
94
+ }
95
+ };
96
+
97
+ /** Explicitly acquire the complete ASR and diarization selection. */
98
+ export function prepareNemotronModels(options = {}) {
99
+ return prepareModels(NEMO_SPEECH_MODELS.stt, options);
100
+ }
101
+
102
+ /** Explicitly acquire Magpie, NanoCodec and the matching tokenizer members. */
103
+ export function prepareMagpieModels(options = {}) {
104
+ return prepareModels(NEMO_SPEECH_MODELS.tts, options);
105
+ }
106
+
107
+ async function prepareModels(selection, {modelAssets, workingDirectory, directories, id = randomUUID(), signal, onProgress, refresh = false}) {
108
+ if (typeof modelAssets?.prepare !== 'function' || typeof modelAssets?.release !== 'function') {
109
+ throw new TypeError('Native speech preparation requires the existing model-assets service.');
110
+ }
111
+ signal?.throwIfAborted();
112
+ let projection;
113
+ if (directories !== undefined && !refresh) {
114
+ if (typeof modelAssets.discover !== 'function' || typeof modelAssets.openExisting !== 'function') {
115
+ throw new TypeError('Installed native speech discovery requires the model-assets discover and openExisting methods.');
116
+ }
117
+ const discovered = await modelAssets.discover({
118
+ directories, signal,
119
+ catalog: [{id: selection.modelId, members: selection.members, archives: selection.archives,
120
+ entryPath: selection.resourcePaths.model}]
121
+ });
122
+ signal?.throwIfAborted();
123
+ const existing = discovered.models.find(function completeSelection(model) {
124
+ return model.complete && model.catalogId === selection.modelId;
125
+ });
126
+ if (existing) {
127
+ await onProgress?.({phase: 'reuse', completed: 0, total: existing.members.length, unit: 'files'});
128
+ signal?.throwIfAborted();
129
+ projection = await modelAssets.openExisting({id, directory: existing.directory, members: existing.members, signal});
130
+ }
131
+ }
132
+ const reused = projection !== undefined;
133
+ projection ??= await modelAssets.prepare({
134
+ id, workingDirectory, members: selection.members,
135
+ archives: selection.archives, signal, onProgress, refresh
136
+ });
137
+ let released;
138
+ function release() {
139
+ released ??= Promise.resolve().then(function releasePreparation() { return modelAssets.release(projection.id); });
140
+ return released;
141
+ }
142
+ try {
143
+ signal?.throwIfAborted();
144
+ if (reused) {
145
+ await onProgress?.({phase: 'ready', completed: projection.members.length, total: projection.members.length, unit: 'files'});
146
+ signal?.throwIfAborted();
147
+ }
148
+ return {
149
+ assetProjectionId: projection.id, directory: projection.directory,
150
+ members: projection.members, resourcePaths: {...selection.resourcePaths}, release
151
+ };
152
+ } catch (error) {
153
+ try {
154
+ await release();
155
+ } catch (cleanupError) {
156
+ throw new AggregateError([error, cleanupError], 'Native speech preparation and cleanup failed.', {cause: error});
157
+ }
158
+ throw error;
159
+ }
160
+ }
@@ -0,0 +1,53 @@
1
+ import path from 'node:path';
2
+ import {fileURLToPath} from 'node:url';
3
+ import {mkdir} from 'node:fs/promises';
4
+ import {runProcess} from '../../process.mjs';
5
+
6
+ /** Build the first-party ASR helper against the caller's installed NeMo SDK. */
7
+ export async function buildNemotronHelper({runtime, outputRoot, signal, onEvent} = {}) {
8
+ if (typeof runtime?.cmakeDirectory !== 'string' || typeof outputRoot !== 'string') {
9
+ throw new TypeError('Nemotron requires the selected runtime cmakeDirectory and helper outputRoot.');
10
+ }
11
+ const root = path.resolve(outputRoot);
12
+ const build = path.join(root, 'build');
13
+ const source = fileURLToPath(
14
+ new URL('./native/', import.meta.url)
15
+ );
16
+ await mkdir(
17
+ root,
18
+ {recursive: true}
19
+ );
20
+ const options = {signal, onEvent};
21
+ const configure = await runProcess(
22
+ 'cmake',
23
+ [
24
+ '-S', source,
25
+ '-B', build,
26
+ `-DNeMoSpeech_DIR=${path.resolve(runtime.cmakeDirectory)}`,
27
+ `-DCMAKE_INSTALL_PREFIX=${root}`,
28
+ '-DCMAKE_BUILD_TYPE=Release'
29
+ ],
30
+ options
31
+ );
32
+ const compile = await runProcess(
33
+ 'cmake',
34
+ ['--build', build, '--config', 'Release'],
35
+ options
36
+ );
37
+ const install = await runProcess(
38
+ 'cmake',
39
+ ['--install', build, '--config', 'Release'],
40
+ options
41
+ );
42
+ const executable = path.join(root, 'bin', process.platform === 'win32' ? 'arcane-nemotron.exe' : 'arcane-nemotron');
43
+ return {
44
+ platform: process.platform,
45
+ architecture: process.arch,
46
+ root,
47
+ executable,
48
+ helperExecutable: executable,
49
+ diagnostics: {configure, compile, install}
50
+ };
51
+ }
52
+
53
+ export default buildNemotronHelper;