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