arcane-os 0.62.0 → 0.64.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 (44) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/core/client.mjs +7 -2
  4. package/docs/reference/README.md +7 -0
  5. package/docs/reference/cli.md +5 -1
  6. package/docs/reference/core-client.md +11 -2
  7. package/docs/reference/core-native-packaging.md +57 -8
  8. package/docs/reference/core-repositories.md +149 -0
  9. package/docs/reference/inventory/package-api.json +163 -9
  10. package/docs/reference/local-ai.md +51 -7
  11. package/docs/reference/local-image-flux.md +154 -0
  12. package/docs/reference/local-image-generation.md +12 -0
  13. package/docs/reference/local-image-sdxl.md +139 -0
  14. package/docs/reference/local-whisper.md +219 -0
  15. package/docs/reference/native-speech.md +163 -0
  16. package/docs/reference/runtime-modules.md +16 -2
  17. package/docs/reference/sdk-api.md +335 -6
  18. package/package.json +5 -1
  19. package/runtime/arcane/components/speech.html +15 -3
  20. package/runtime/arcane/components/voice-transcription.html +19 -3
  21. package/runtime/arcane/modules/AI.js +250 -27
  22. package/runtime/arcane/modules/AIProviderRuntime.js +20 -3
  23. package/runtime/arcane/modules/AIRuntimeState.js +15 -2
  24. package/runtime/arcane/modules/ComponentContracts.js +3 -0
  25. package/schemas/arcane-app.schema.json +53 -2
  26. package/schemas/arcane-package.schema.json +53 -2
  27. package/src/core/repositories.mjs +210 -0
  28. package/src/core/services/speech.mjs +361 -0
  29. package/src/local-ai/config.mjs +9 -3
  30. package/src/local-ai/dev.mjs +32 -2
  31. package/src/local-ai/image-library.mjs +9 -0
  32. package/src/local-ai/image-models/flux.mjs +31 -0
  33. package/src/local-ai/image-models/sdxl.mjs +18 -0
  34. package/src/local-ai/install.mjs +26 -7
  35. package/src/local-ai/whisper/build.mjs +48 -0
  36. package/src/local-ai/whisper/config.mjs +61 -0
  37. package/src/local-ai/whisper/index.mjs +515 -0
  38. package/src/local-ai/whisper/install.mjs +274 -0
  39. package/src/local-ai/whisper/native/CMakeLists.txt +98 -0
  40. package/src/local-ai/whisper/native/NOTICE +18 -0
  41. package/src/local-ai/whisper/native/README.md +104 -0
  42. package/src/local-ai/whisper/native/main.cpp +489 -0
  43. package/src/native/portable-layout.mjs +37 -7
  44. package/src/native/windows-provider.mjs +39 -15
@@ -0,0 +1,489 @@
1
+ #include <whisper.h>
2
+ #include <ggml-backend.h>
3
+
4
+ #include <atomic>
5
+ #include <condition_variable>
6
+ #include <cstdint>
7
+ #include <cstring>
8
+ #include <filesystem>
9
+ #include <fstream>
10
+ #include <iomanip>
11
+ #include <iostream>
12
+ #include <limits>
13
+ #include <locale>
14
+ #include <memory>
15
+ #include <mutex>
16
+ #include <sstream>
17
+ #include <stdexcept>
18
+ #include <string>
19
+ #include <thread>
20
+ #include <vector>
21
+
22
+ #ifdef _WIN32
23
+ #include <fcntl.h>
24
+ #include <io.h>
25
+ #endif
26
+
27
+ namespace {
28
+
29
+ struct Configuration {
30
+ std::string model;
31
+ std::string runtime;
32
+ std::string backend = "gpu";
33
+ int threads = 1;
34
+ };
35
+
36
+ struct Segment {
37
+ int index;
38
+ std::string text;
39
+ double start;
40
+ double end;
41
+ };
42
+
43
+ struct Request {
44
+ std::string id;
45
+ std::string audio_path;
46
+ std::string language;
47
+ bool translate = false;
48
+ std::atomic<bool> cancelled{false};
49
+ std::vector<Segment> segments;
50
+ std::string text;
51
+ std::string detected_language;
52
+ std::string error;
53
+ std::string error_code;
54
+ double duration = 0;
55
+ };
56
+
57
+ struct Commands {
58
+ std::mutex mutex;
59
+ std::condition_variable changed;
60
+ std::shared_ptr<Request> active;
61
+ bool stopping = false;
62
+ };
63
+
64
+ struct NativeLog {
65
+ std::mutex mutex;
66
+ std::string pending;
67
+ std::string backend;
68
+
69
+ ~NativeLog() {
70
+ whisper_log_set(nullptr, nullptr);
71
+ }
72
+ };
73
+
74
+ std::mutex output_mutex;
75
+
76
+ std::string json_string(const std::string& value) {
77
+ std::ostringstream output;
78
+ output << '"';
79
+ for (const unsigned char character : value) {
80
+ switch (character) {
81
+ case '"': output << "\\\""; break;
82
+ case '\\': output << "\\\\"; break;
83
+ case '\n': output << "\\n"; break;
84
+ case '\r': output << "\\r"; break;
85
+ case '\t': output << "\\t"; break;
86
+ default:
87
+ if (character < 0x20) {
88
+ output << "\\u" << std::hex << std::setw(4) << std::setfill('0')
89
+ << static_cast<unsigned int>(character) << std::dec;
90
+ } else {
91
+ output << character;
92
+ }
93
+ }
94
+ }
95
+ output << '"';
96
+ return output.str();
97
+ }
98
+
99
+ std::string decode_base64(const std::string& encoded) {
100
+ const std::string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
101
+ std::string decoded;
102
+ uint32_t accumulator = 0;
103
+ int remaining = 0;
104
+ bool padding = false;
105
+ for (const char character : encoded) {
106
+ if (character == '=') {
107
+ padding = true;
108
+ continue;
109
+ }
110
+ const auto digit = alphabet.find(character);
111
+ if (padding || digit == std::string::npos) {
112
+ throw std::runtime_error("Unreadable base64 command field.");
113
+ }
114
+ accumulator = (accumulator << 6) | static_cast<uint32_t>(digit);
115
+ remaining += 6;
116
+ if (remaining >= 8) {
117
+ remaining -= 8;
118
+ decoded.push_back(static_cast<char>((accumulator >> remaining) & 0xff));
119
+ }
120
+ }
121
+ return decoded;
122
+ }
123
+
124
+ void emit(const std::string& record) {
125
+ const std::lock_guard<std::mutex> lock(output_mutex);
126
+ std::cout << record << '\n' << std::flush;
127
+ }
128
+
129
+ void emit_error(const std::string& request_id, const std::string& message, const std::string& code = "") {
130
+ emit("{\"type\":\"error\",\"requestId\":" + json_string(request_id)
131
+ + ",\"message\":" + json_string(message)
132
+ + (code.empty() ? "" : ",\"code\":" + json_string(code)) + "}");
133
+ }
134
+
135
+ std::string segment_json(const Segment& segment) {
136
+ std::ostringstream output;
137
+ output.imbue(std::locale::classic());
138
+ output << std::setprecision(std::numeric_limits<double>::max_digits10)
139
+ << "{\"index\":" << segment.index << ",\"text\":" << json_string(segment.text)
140
+ << ",\"start\":" << segment.start << ",\"end\":" << segment.end << '}';
141
+ return output.str();
142
+ }
143
+
144
+ Segment read_segment(whisper_state* state, int index) {
145
+ return Segment{
146
+ index,
147
+ whisper_full_get_segment_text_from_state(state, index),
148
+ whisper_full_get_segment_t0_from_state(state, index) * 0.01,
149
+ whisper_full_get_segment_t1_from_state(state, index) * 0.01
150
+ };
151
+ }
152
+
153
+ void on_segment(whisper_context*, whisper_state* state, int added, void* data) {
154
+ Request& request = *static_cast<Request*>(data);
155
+ const int total = whisper_full_n_segments_from_state(state);
156
+ for (int index = total - added; index < total; ++index) {
157
+ const Segment segment = read_segment(state, index);
158
+ emit("{\"type\":\"segment\",\"requestId\":" + json_string(request.id)
159
+ + ",\"index\":" + std::to_string(segment.index)
160
+ + ",\"text\":" + json_string(segment.text)
161
+ + ",\"start\":" + std::to_string(segment.start)
162
+ + ",\"end\":" + std::to_string(segment.end) + "}");
163
+ }
164
+ }
165
+
166
+ void on_progress(whisper_context*, whisper_state*, int progress, void* data) {
167
+ const Request& request = *static_cast<Request*>(data);
168
+ emit("{\"type\":\"progress\",\"requestId\":" + json_string(request.id)
169
+ + ",\"progress\":" + std::to_string(progress) + "}");
170
+ }
171
+
172
+ bool should_abort(void* data) {
173
+ return static_cast<Request*>(data)->cancelled.load();
174
+ }
175
+
176
+ bool before_encoder(whisper_context*, whisper_state*, void* data) {
177
+ return !should_abort(data);
178
+ }
179
+
180
+ void native_log(ggml_log_level, const char* text, void* data) {
181
+ NativeLog& log = *static_cast<NativeLog*>(data);
182
+ const std::lock_guard<std::mutex> lock(log.mutex);
183
+ std::cerr << text << std::flush;
184
+ log.pending += text;
185
+ std::string::size_type newline;
186
+ while ((newline = log.pending.find('\n')) != std::string::npos) {
187
+ const std::string line = log.pending.substr(0, newline);
188
+ log.pending.erase(0, newline + 1);
189
+ const std::string selected = "whisper_backend_init_gpu: using ";
190
+ const std::string failed = "whisper_backend_init_gpu: failed to initialize ";
191
+ if (line.compare(0, selected.size(), selected) == 0) {
192
+ const auto end = line.rfind(" backend");
193
+ if (end != std::string::npos) {
194
+ log.backend = line.substr(selected.size(), end - selected.size());
195
+ }
196
+ } else if (line.compare(0, failed.size(), failed) == 0
197
+ || line == "whisper_backend_init_gpu: no GPU found") {
198
+ log.backend = "CPU";
199
+ }
200
+ }
201
+ }
202
+
203
+ std::vector<float> read_audio(Request& request) {
204
+ std::ifstream input(std::filesystem::u8path(request.audio_path), std::ios::binary);
205
+ if (!input) {
206
+ throw std::runtime_error("Could not open the selected decoded audio: " + request.audio_path);
207
+ }
208
+ std::vector<float> audio;
209
+ unsigned char sample[4];
210
+ while (input.read(reinterpret_cast<char*>(sample), sizeof(sample))) {
211
+ if (request.cancelled.load()) {
212
+ return {};
213
+ }
214
+ // This is the decoder's f32le transport representation, not a content
215
+ // policy or an audio normalization step.
216
+ const uint32_t bits = static_cast<uint32_t>(sample[0])
217
+ | (static_cast<uint32_t>(sample[1]) << 8)
218
+ | (static_cast<uint32_t>(sample[2]) << 16)
219
+ | (static_cast<uint32_t>(sample[3]) << 24);
220
+ float value;
221
+ std::memcpy(&value, &bits, sizeof(value));
222
+ audio.push_back(value);
223
+ }
224
+ if (input.bad() || input.gcount() != 0) {
225
+ throw std::runtime_error("The decoded audio ended inside an f32le sample or could not be read.");
226
+ }
227
+ return audio;
228
+ }
229
+
230
+ void transcribe(whisper_context* context, const Configuration& configuration, Request& request) {
231
+ try {
232
+ if (request.cancelled.load()) {
233
+ return;
234
+ }
235
+ const std::vector<float> audio = read_audio(request);
236
+ if (request.cancelled.load()) {
237
+ return;
238
+ }
239
+ request.duration = static_cast<double>(audio.size()) / WHISPER_SAMPLE_RATE;
240
+ if (audio.size() > static_cast<size_t>(std::numeric_limits<int>::max())) {
241
+ throw std::runtime_error("The complete recording exceeds whisper_full's native signed-int sample argument; no audio was submitted or shortened.");
242
+ }
243
+ if (audio.empty()) {
244
+ throw std::runtime_error("The decoder returned no audio samples.");
245
+ }
246
+ if (!request.language.empty() && request.language != "auto"
247
+ && whisper_lang_id(request.language.c_str()) < 0) {
248
+ throw std::runtime_error("Whisper does not recognize the requested language: " + request.language);
249
+ }
250
+ whisper_full_params parameters = whisper_full_default_params(WHISPER_SAMPLING_GREEDY);
251
+ parameters.n_threads = configuration.threads;
252
+ parameters.offset_ms = 0;
253
+ parameters.duration_ms = 0;
254
+ parameters.no_context = true;
255
+ parameters.translate = request.translate;
256
+ parameters.language = request.language.c_str();
257
+ parameters.print_realtime = false;
258
+ parameters.print_progress = false;
259
+ parameters.print_timestamps = false;
260
+ parameters.new_segment_callback = on_segment;
261
+ parameters.new_segment_callback_user_data = &request;
262
+ parameters.progress_callback = on_progress;
263
+ parameters.progress_callback_user_data = &request;
264
+ parameters.encoder_begin_callback = before_encoder;
265
+ parameters.encoder_begin_callback_user_data = &request;
266
+ parameters.abort_callback = should_abort;
267
+ parameters.abort_callback_user_data = &request;
268
+ const int result = whisper_full(context, parameters, audio.data(), static_cast<int>(audio.size()));
269
+ if (request.cancelled.load()) {
270
+ return;
271
+ }
272
+ if (result != 0) {
273
+ request.error_code = "WHISPER_INFERENCE_FAILED";
274
+ throw std::runtime_error("whisper_full returned " + std::to_string(result) + ".");
275
+ }
276
+ const char* language = whisper_lang_str(whisper_full_lang_id(context));
277
+ request.detected_language = language == nullptr ? "" : language;
278
+ const int total = whisper_full_n_segments(context);
279
+ for (int index = 0; index < total; ++index) {
280
+ Segment segment{
281
+ index,
282
+ whisper_full_get_segment_text(context, index),
283
+ whisper_full_get_segment_t0(context, index) * 0.01,
284
+ whisper_full_get_segment_t1(context, index) * 0.01
285
+ };
286
+ request.text += segment.text;
287
+ request.segments.push_back(std::move(segment));
288
+ }
289
+ } catch (const std::exception& error) {
290
+ request.error = error.what();
291
+ }
292
+ }
293
+
294
+ void emit_result(const Request& request) {
295
+ if (request.cancelled.load()) {
296
+ emit("{\"type\":\"cancelled\",\"requestId\":" + json_string(request.id) + "}");
297
+ return;
298
+ }
299
+ if (!request.error.empty()) {
300
+ emit_error(request.id, request.error, request.error_code);
301
+ return;
302
+ }
303
+ std::ostringstream output;
304
+ output.imbue(std::locale::classic());
305
+ output << std::setprecision(std::numeric_limits<double>::max_digits10)
306
+ << "{\"type\":\"complete\",\"requestId\":" << json_string(request.id)
307
+ << ",\"language\":" << json_string(request.detected_language)
308
+ << ",\"duration\":" << request.duration
309
+ << ",\"text\":" << json_string(request.text) << ",\"segments\":[";
310
+ for (size_t index = 0; index < request.segments.size(); ++index) {
311
+ if (index != 0) {
312
+ output << ',';
313
+ }
314
+ output << segment_json(request.segments[index]);
315
+ }
316
+ output << "]}";
317
+ emit(output.str());
318
+ }
319
+
320
+ std::vector<std::string> command_fields(const std::string& line) {
321
+ std::vector<std::string> fields;
322
+ size_t start = 0;
323
+ while (true) {
324
+ const size_t separator = line.find('\t', start);
325
+ fields.push_back(line.substr(start, separator == std::string::npos ? separator : separator - start));
326
+ if (separator == std::string::npos) {
327
+ return fields;
328
+ }
329
+ start = separator + 1;
330
+ }
331
+ }
332
+
333
+ void read_commands(Commands& commands) {
334
+ std::string line;
335
+ while (std::getline(std::cin, line)) {
336
+ std::string request_id;
337
+ try {
338
+ const std::vector<std::string> fields = command_fields(line);
339
+ if (fields.size() > 1) {
340
+ request_id = fields[1];
341
+ }
342
+ std::unique_lock<std::mutex> lock(commands.mutex);
343
+ if (fields[0] == "shutdown") {
344
+ break;
345
+ }
346
+ if (fields[0] == "cancel" && fields.size() == 2) {
347
+ if (commands.active && commands.active->id == request_id) {
348
+ commands.active->cancelled.store(true);
349
+ }
350
+ } else if (fields[0] == "transcribe" && fields.size() == 5) {
351
+ if (commands.active) {
352
+ throw std::runtime_error("The retained model is already transcribing an active request.");
353
+ }
354
+ const auto request = std::make_shared<Request>();
355
+ request->id = request_id;
356
+ request->audio_path = decode_base64(fields[2]);
357
+ request->language = decode_base64(fields[3]);
358
+ if (fields[4] != "0" && fields[4] != "1") {
359
+ throw std::runtime_error("Unreadable translation command field.");
360
+ }
361
+ request->translate = fields[4] == "1";
362
+ commands.active = request;
363
+ emit("{\"type\":\"accepted\",\"requestId\":" + json_string(request_id) + "}");
364
+ commands.changed.notify_one();
365
+ } else {
366
+ throw std::runtime_error("Unreadable Whisper command.");
367
+ }
368
+ } catch (const std::exception& error) {
369
+ emit_error(request_id, error.what());
370
+ }
371
+ }
372
+ const std::lock_guard<std::mutex> lock(commands.mutex);
373
+ commands.stopping = true;
374
+ if (commands.active) {
375
+ commands.active->cancelled.store(true);
376
+ }
377
+ commands.changed.notify_one();
378
+ }
379
+
380
+ Configuration read_configuration(int argc, char** argv) {
381
+ Configuration configuration;
382
+ const unsigned int available = std::thread::hardware_concurrency();
383
+ configuration.threads = available == 0 ? 1 : static_cast<int>(available);
384
+ for (int index = 1; index < argc; index += 2) {
385
+ if (index + 1 == argc) {
386
+ throw std::runtime_error("A helper argument is missing its value.");
387
+ }
388
+ const std::string option = argv[index];
389
+ const std::string value = argv[index + 1];
390
+ if (option == "--model-base64") {
391
+ configuration.model = decode_base64(value);
392
+ } else if (option == "--runtime-base64") {
393
+ configuration.runtime = decode_base64(value);
394
+ } else if (option == "--backend") {
395
+ configuration.backend = value;
396
+ } else if (option == "--threads") {
397
+ configuration.threads = std::stoi(value);
398
+ } else {
399
+ throw std::runtime_error("Unknown helper argument: " + option);
400
+ }
401
+ }
402
+ if (configuration.model.empty() || configuration.runtime.empty()) {
403
+ throw std::runtime_error("The selected model and runtime paths are required.");
404
+ }
405
+ if (configuration.backend != "gpu" && configuration.backend != "cpu") {
406
+ throw std::runtime_error("The requested backend must be gpu or cpu.");
407
+ }
408
+ if (configuration.threads < 1) {
409
+ throw std::runtime_error("Native inference requires a positive thread count.");
410
+ }
411
+ return configuration;
412
+ }
413
+
414
+ int serve(const Configuration& configuration) {
415
+ NativeLog log;
416
+ whisper_log_set(native_log, &log);
417
+ emit("{\"type\":\"loading\",\"model\":" + json_string(configuration.model)
418
+ + ",\"requestedBackend\":" + json_string(configuration.backend)
419
+ + ",\"observedBackend\":null,\"backendEvidence\":null}");
420
+ ggml_backend_load_all_from_path(configuration.runtime.c_str());
421
+ whisper_context_params parameters = whisper_context_default_params();
422
+ parameters.use_gpu = configuration.backend == "gpu";
423
+ using Context = std::unique_ptr<whisper_context, decltype(&whisper_free)>;
424
+ Context context(whisper_init_from_file_with_params(configuration.model.c_str(), parameters), whisper_free);
425
+ if (!context) {
426
+ throw std::runtime_error("Whisper could not load the selected model.");
427
+ }
428
+ {
429
+ const std::lock_guard<std::mutex> lock(log.mutex);
430
+ // The public C API exposes available devices, not a context backend
431
+ // getter. This observation is explicitly attributed to completed native
432
+ // initialization logs; it does not claim every operation uses a GPU.
433
+ emit("{\"type\":\"ready\",\"model\":" + json_string(configuration.model)
434
+ + ",\"requestedBackend\":" + json_string(configuration.backend)
435
+ + ",\"observedBackend\":" + (log.backend.empty() ? "null" : json_string(log.backend))
436
+ + ",\"backendEvidence\":" + (log.backend.empty() ? "null" : "\"runtime-log\"") + "}");
437
+ }
438
+ Commands commands;
439
+ std::thread reader(read_commands, std::ref(commands));
440
+ while (true) {
441
+ std::shared_ptr<Request> request;
442
+ {
443
+ std::unique_lock<std::mutex> lock(commands.mutex);
444
+ commands.changed.wait(lock, [&commands] { return commands.stopping || commands.active != nullptr; });
445
+ request = commands.active;
446
+ if (!request) {
447
+ break;
448
+ }
449
+ }
450
+ // The command reader stays responsive while this exact worker owns the
451
+ // opaque context. Joining precedes every terminal event and model reuse.
452
+ try {
453
+ std::thread inference(transcribe, context.get(), std::cref(configuration), std::ref(*request));
454
+ inference.join();
455
+ } catch (const std::exception& error) {
456
+ request->error = error.what();
457
+ }
458
+ {
459
+ const std::lock_guard<std::mutex> lock(commands.mutex);
460
+ emit_result(*request);
461
+ commands.active.reset();
462
+ if (commands.stopping) {
463
+ break;
464
+ }
465
+ }
466
+ }
467
+ reader.join();
468
+ context.reset();
469
+ whisper_log_set(nullptr, nullptr);
470
+ emit("{\"type\":\"stopped\"}");
471
+ return 0;
472
+ }
473
+
474
+ } // namespace
475
+
476
+ int main(int argc, char** argv) {
477
+ #ifdef _WIN32
478
+ _setmode(_fileno(stdin), _O_BINARY);
479
+ _setmode(_fileno(stdout), _O_BINARY);
480
+ _setmode(_fileno(stderr), _O_BINARY);
481
+ #endif
482
+ try {
483
+ return serve(read_configuration(argc, argv));
484
+ } catch (const std::exception& error) {
485
+ whisper_log_set(nullptr, nullptr);
486
+ emit_error("", error.what());
487
+ return 1;
488
+ }
489
+ }
@@ -153,6 +153,9 @@ export function coreEntrySource(application, version, services, {localAI, runtim
153
153
  const imageSelected = localAI?.runtimes?.some(function selectedImage(requirement) {
154
154
  return (typeof requirement === 'string' ? requirement : requirement.id) === 'stable-diffusion.cpp';
155
155
  }) ?? false;
156
+ const whisperSelected = localAI?.runtimes?.some(function selectedWhisper(requirement) {
157
+ return (typeof requirement === 'string' ? requirement : requirement.id) === 'whisper.cpp';
158
+ }) ?? false;
156
159
  const imports = services.map(function serviceImport(service, index) {
157
160
  const specifier = '../app/' + service.module.split('/').map(encodeURIComponent).join('/');
158
161
  return `import createService${index} from ${JSON.stringify(specifier)};`;
@@ -165,7 +168,8 @@ export function coreEntrySource(application, version, services, {localAI, runtim
165
168
  const chatConfiguration = {
166
169
  ...localAI,
167
170
  runtimes: localAI.runtimes.filter(function selectedChat(requirement) {
168
- return (typeof requirement === 'string' ? requirement : requirement.id) !== 'stable-diffusion.cpp';
171
+ const id = typeof requirement === 'string' ? requirement : requirement.id;
172
+ return id !== 'stable-diffusion.cpp' && id !== 'whisper.cpp';
169
173
  })
170
174
  };
171
175
  imports.push("import {createLocalAIService} from 'arcane-os/core/local-ai';");
@@ -177,6 +181,12 @@ export function coreEntrySource(application, version, services, {localAI, runtim
177
181
  definitions.push(' modelAssets');
178
182
  definitions.push(` createLocalImageService(JSON.parse(${JSON.stringify(JSON.stringify(localAI))}), {...context, modelAssets})`);
179
183
  }
184
+ if (whisperSelected) {
185
+ imports.push("import path from 'node:path';");
186
+ imports.push("import {createSpeechService} from 'arcane-os/core/speech';");
187
+ imports.push("import {createWhisperRuntime} from 'arcane-os/local-ai/whisper';");
188
+ definitions.push(' createSpeechService({stt, signal: context.signal})');
189
+ }
180
190
  if (packagedWeb) {
181
191
  imports.push("import {createPackagedWebService} from 'arcane-os/core/packaged-web';");
182
192
  definitions.push(" createPackagedWebService({artifactRoot: fileURLToPath(new URL('../', import.meta.url))}, context)");
@@ -200,18 +210,26 @@ export function coreEntrySource(application, version, services, {localAI, runtim
200
210
  ...(localAI === undefined ? [] : [
201
211
  ` runtimes: JSON.parse(${JSON.stringify(JSON.stringify(runtimes))}).map(function runtimeLocation(runtime) {`,
202
212
  ' const resolved = {...runtime};',
203
- " for (const field of ['root', 'executable', 'modulePath', 'includeDirectory', 'libraryDirectory', 'binaryDirectory', 'cmakeDirectory', 'libraryPath', 'bindingModulePath']) {",
213
+ " for (const field of ['root', 'executable', 'modulePath', 'includeDirectory', 'libraryDirectory', 'binaryDirectory', 'cmakeDirectory', 'libraryPath', 'bindingModulePath', 'helperExecutable', 'helperRoot', 'decoderExecutable', 'decoderRoot']) {",
204
214
  ' if (runtime[field] !== undefined) resolved[field] = runtimePath(runtime[field]);',
205
215
  ' }',
206
216
  ' if (runtime.variants) resolved.variants = runtime.variants.map(function nativeVariant(variant) {',
207
- ' return {...variant, root: runtimePath(variant.root), libraryPath: runtimePath(variant.libraryPath)};',
217
+ ' const resolvedVariant = {...variant};',
218
+ " for (const field of ['root', 'libraryPath', 'libraryDirectory', 'executable']) {",
219
+ ' if (variant[field] !== undefined) resolvedVariant[field] = runtimePath(variant[field]);',
220
+ ' }',
221
+ ' return resolvedVariant;',
208
222
  ' });',
209
223
  ' if (runtime.models) resolved.models = runtime.models.map(function nativeModel(model) {',
210
- ' const resources = {};',
211
- ' for (const [role, resource] of Object.entries(model.resources)) {',
212
- " resources[role] = typeof resource === 'string' ? runtimePath(resource) : resource;",
224
+ ' const resolvedModel = {...model};',
225
+ ' if (model.path !== undefined) resolvedModel.path = runtimePath(model.path);',
226
+ ' if (model.resources !== undefined) {',
227
+ ' resolvedModel.resources = {};',
228
+ ' for (const [role, resource] of Object.entries(model.resources)) {',
229
+ " resolvedModel.resources[role] = typeof resource === 'string' ? runtimePath(resource) : resource;",
230
+ ' }',
213
231
  ' }',
214
- ' return {...model, resources};',
232
+ ' return resolvedModel;',
215
233
  ' });',
216
234
  ' return resolved;',
217
235
  ' }),'
@@ -220,6 +238,18 @@ export function coreEntrySource(application, version, services, {localAI, runtim
220
238
  '};',
221
239
  '',
222
240
  ...(imageSelected ? ['const modelAssets = createModelAssetService({appRoot: context.appRoot});', ''] : []),
241
+ ...(whisperSelected ? [
242
+ 'const stt = createWhisperRuntime(',
243
+ ' {',
244
+ ' runtime: context.runtimes.find(function selectedWhisperRuntime(runtime) {',
245
+ " return runtime.id === 'whisper.cpp';",
246
+ ' }),',
247
+ " temporaryDirectory: path.join(context.stateRoot ?? path.join(context.appRoot, '.arcane'), 'speech', 'whisper'),",
248
+ ' onEvent: context.onEvent',
249
+ ' }',
250
+ ');',
251
+ ''
252
+ ] : []),
223
253
  'const host = startCoreHost({',
224
254
  ` application: JSON.parse(${JSON.stringify(JSON.stringify(application))}),`,
225
255
  ` version: ${JSON.stringify(version)},`,