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,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
|