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.
- package/CHANGELOG.md +36 -0
- package/README.md +1 -1
- package/browser-runtime/ai/core-model-assets.mjs +74 -0
- package/browser-runtime/core/client.mjs +195 -0
- package/docs/reference/README.md +9 -0
- package/docs/reference/ai/browser-speech.md +61 -10
- package/docs/reference/core-client.md +35 -0
- package/docs/reference/core-native-packaging.md +38 -6
- package/docs/reference/inventory/package-api.json +133 -5
- package/docs/reference/inventory/runtime-modules.json +39 -13
- package/docs/reference/model-assets.md +164 -3
- package/docs/reference/native-magpie.md +98 -0
- package/docs/reference/native-nemotron.md +204 -0
- package/docs/reference/native-speech.md +250 -5
- package/docs/reference/runtime-modules.md +157 -11
- package/docs/reference/sdk-api.md +247 -4
- package/package.json +7 -3
- 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/host.mjs +1 -0
- package/src/core/services/local-ai.mjs +46 -12
- package/src/core/services/model-assets.mjs +491 -59
- package/src/core/services/speech.mjs +237 -4
- 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/nemo-models.mjs +160 -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 +693 -0
- package/src/local-ai/nemotron/native/CMakeLists.txt +17 -0
- package/src/local-ai/nemotron/native/main.cpp +282 -0
- package/src/native/macos-provider.mjs +14 -7
- package/src/native/portable-assembly.mjs +180 -0
- package/src/native/portable-provider.mjs +17 -161
- package/src/native/publish-output.mjs +101 -0
- 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 = ⊧
|
|
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;
|