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,274 @@
1
+ import {createWriteStream} from 'node:fs';
2
+ import {copyFile, cp, mkdir, readdir, stat, unlink} from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import {Readable} from 'node:stream';
5
+ import {pipeline} from 'node:stream/promises';
6
+ import Is from 'strong-type';
7
+ import packageMetadata from '../../../package.json' with {type: 'json'};
8
+ import {ArcaneError, ERROR_CODES, throwIfAborted} from '../../errors.mjs';
9
+ import {extractLocalAIArchive} from '../archive.mjs';
10
+
11
+ const is = new Is(false);
12
+
13
+ /** Functional selection keys share preparation, never identify model content. */
14
+ export function whisperRuntimeSelection(requirement, includeModels = true) {
15
+ const {models, modelId, helperRoot, helperVersion, ...runtime} = requirement;
16
+ return JSON.stringify({
17
+ ...runtime,
18
+ ...(includeModels ? {models, modelId} : {}),
19
+ helperVersion: helperVersion ?? packageMetadata.version,
20
+ helperRoot: helperRoot ?? (is.string(requirement.helperExecutable) ? path.dirname(requirement.helperExecutable) : undefined)
21
+ });
22
+ }
23
+
24
+ export async function installedWhisperRuntime(record, signal, includeModels = true) {
25
+ if (!is.array(record?.variants) || record.variants.length === 0 || !is.array(record.models)) return false;
26
+ const files = [record.helperExecutable, record.decoderExecutable];
27
+ const directories = [record.root, record.helperRoot ?? (is.string(record.helperExecutable) ? path.dirname(record.helperExecutable) : undefined), record.decoderRoot];
28
+ for (const variant of record.variants) {
29
+ files.push(variant.executable);
30
+ directories.push(variant.root, variant.libraryDirectory);
31
+ }
32
+ if (includeModels) for (const model of record.models) files.push(model.path);
33
+ for (const location of directories) {
34
+ throwIfAborted(signal);
35
+ if (!is.string(location) || !(await stat(location)).isDirectory()) return false;
36
+ }
37
+ for (const location of files) {
38
+ throwIfAborted(signal);
39
+ if (!is.string(location) || !(await stat(location)).isFile()) return false;
40
+ }
41
+ return true;
42
+ }
43
+
44
+ async function findWhisperFile(root, filename, signal) {
45
+ const directories = [root];
46
+ for (const directory of directories) {
47
+ throwIfAborted(signal);
48
+ for (const entry of await readdir(directory, {withFileTypes: true})) {
49
+ const location = path.join(directory, entry.name);
50
+ if (entry.name === filename && (entry.isFile() || entry.isSymbolicLink())) return location;
51
+ if (entry.isDirectory()) directories.push(location);
52
+ }
53
+ }
54
+ throw new ArcaneError(ERROR_CODES.operationFailed, `The selected Whisper dependency tree does not contain ${filename}.`);
55
+ }
56
+
57
+ async function downloadWhisperResource(url, filename, {signal, onEvent, upstreamResponse}) {
58
+ await onEvent({
59
+ type: 'local-ai.install.downloading',
60
+ message: `Downloading ${path.basename(filename)}.`,
61
+ data: {id: 'whisper.cpp', asset: path.basename(filename)}
62
+ });
63
+ const response = await upstreamResponse(url, signal);
64
+ await pipeline(Readable.fromWeb(response.body), createWriteStream(filename, {flags: 'wx'}), {signal});
65
+ }
66
+
67
+ async function prepareWhisperArchive(url, root, options) {
68
+ const filename = path.posix.basename(new URL(url).pathname);
69
+ const archive = path.join(path.dirname(root), `${path.basename(root)}-${filename}`);
70
+ await downloadWhisperResource(url, archive, options);
71
+ await options.onEvent({
72
+ type: 'local-ai.install.extracting',
73
+ message: `Extracting ${filename}.`,
74
+ data: {id: 'whisper.cpp', asset: filename}
75
+ });
76
+ await extractLocalAIArchive({archive, directory: root, signal: options.signal, onEvent: options.onEvent});
77
+ await unlink(archive);
78
+ }
79
+
80
+ async function copyWhisperTree(source, destination, signal) {
81
+ throwIfAborted(signal);
82
+ await cp(path.resolve(source), destination, {
83
+ recursive: true,
84
+ verbatimSymlinks: true,
85
+ filter: function retainCompleteWhisperTree() {
86
+ throwIfAborted(signal);
87
+ return true;
88
+ }
89
+ });
90
+ throwIfAborted(signal);
91
+ }
92
+
93
+ function copiedWhisperLocation(sourceRoot, destinationRoot, location) {
94
+ const root = path.resolve(sourceRoot);
95
+ const relative = path.relative(root, path.resolve(root, location));
96
+ if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
97
+ throw new ArcaneError(ERROR_CODES.usage, 'A Whisper library, helper, or decoder path must belong to its selected complete runtime tree so the native bundle can retain it.');
98
+ }
99
+ return path.join(destinationRoot, relative);
100
+ }
101
+
102
+ function defaultWhisperVariants(requirement, platform, architecture) {
103
+ if (requirement.variants) return requirement.variants;
104
+ if (platform !== 'win32' || architecture !== 'x64' || requirement.version !== '1.9.4') {
105
+ throw new ArcaneError(ERROR_CODES.targetUnavailable, `Automatic Whisper distributions are available for 1.9.4 on win32/x64. Supply complete runtime variants for ${requirement.version} on ${platform}/${architecture}.`);
106
+ }
107
+ if (requirement.backend === 'metal') {
108
+ throw new ArcaneError(ERROR_CODES.targetUnavailable, 'The selected Windows Whisper distribution has no Metal backend.');
109
+ }
110
+ return [
111
+ ...(requirement.backend === 'cpu' ? [] : [{
112
+ backend: 'cuda',
113
+ url: 'https://github.com/ggml-org/whisper.cpp/releases/download/b5130/whisper-cublas-12.4.0-bin-x64.zip'
114
+ }]),
115
+ {
116
+ backend: 'cpu',
117
+ url: 'https://github.com/ggml-org/whisper.cpp/releases/download/b5130/whisper-bin-x64.zip'
118
+ }
119
+ ];
120
+ }
121
+
122
+ /** Prepare complete runtime trees under the shared install attempt owner. */
123
+ export async function installWhisperDistribution(requirement, options) {
124
+ const {root, platform, architecture, onEvent, reusable} = options;
125
+ const controller = new AbortController();
126
+ const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
127
+ const preparation = {...options, signal};
128
+ const variants = reusable?.runtime.variants ?? defaultWhisperVariants(requirement, platform, architecture);
129
+ if (!requirement.helperExecutable && !requirement.helperUrl && (platform !== 'win32' || architecture !== 'x64')) {
130
+ throw new ArcaneError(ERROR_CODES.targetUnavailable, `Supply the Arcane Whisper helperExecutable or helperUrl for ${platform}/${architecture}.`);
131
+ }
132
+ if (!requirement.decoderExecutable && !requirement.decoderUrl && (platform !== 'win32' || architecture !== 'x64')) {
133
+ throw new ArcaneError(ERROR_CODES.targetUnavailable, `Supply a complete decoder tree through decoderExecutable and decoderRoot, or decoderUrl, for ${platform}/${architecture}.`);
134
+ }
135
+ const helperName = `arcane-whisper${platform === 'win32' ? '.exe' : ''}`;
136
+ const helperRoot = path.join(root, 'helper');
137
+ let helperExecutable = path.join(helperRoot, helperName);
138
+ const decoderRoot = path.join(root, 'decoder');
139
+ const models = [];
140
+ const preparedVariants = [];
141
+ let decoderExecutable;
142
+ await mkdir(root, {recursive: true});
143
+
144
+ const jobs = variants.map(function variantPreparation(variant, index) {
145
+ return async function prepareSelectedVariant() {
146
+ const variantRoot = path.join(root, `variant-${index}-${variant.backend}`);
147
+ const selectedLibraryDirectory = variant.libraryDirectory
148
+ ? copiedWhisperLocation(variant.root ?? variantRoot, variantRoot, variant.libraryDirectory)
149
+ : undefined;
150
+ if (variant.root) await copyWhisperTree(variant.root, variantRoot, signal);
151
+ else await prepareWhisperArchive(variant.url, variantRoot, preparation);
152
+ const libraryName = platform === 'win32' ? 'whisper.dll' : platform === 'darwin' ? 'libwhisper.dylib' : 'libwhisper.so';
153
+ const libraryDirectory = selectedLibraryDirectory ?? path.dirname(await findWhisperFile(variantRoot, libraryName, signal));
154
+ preparedVariants[index] = {backend: variant.backend, root: variantRoot, libraryDirectory};
155
+ };
156
+ });
157
+ jobs.push(async function prepareHelper() {
158
+ const existingHelper = reusable?.runtime.helperExecutable ?? requirement.helperExecutable;
159
+ if (existingHelper) {
160
+ const source = path.resolve(existingHelper);
161
+ const sourceRoot = path.resolve(reusable
162
+ ? reusable.runtime.helperRoot ?? path.dirname(source)
163
+ : requirement.helperRoot ?? path.dirname(source));
164
+ helperExecutable = copiedWhisperLocation(sourceRoot, helperRoot, source);
165
+ await copyWhisperTree(sourceRoot, helperRoot, signal);
166
+ return;
167
+ }
168
+ const helperVersion = requirement.helperVersion ?? packageMetadata.version;
169
+ const helperUrl = requirement.helperUrl ?? `https://github.com/TheWizardNexus/arcane-os-sdk/releases/download/${helperVersion}/arcane-whisper-windows-x64.tar.gz`;
170
+ await prepareWhisperArchive(helperUrl, helperRoot, preparation);
171
+ helperExecutable = await findWhisperFile(helperRoot, helperName, signal);
172
+ });
173
+ jobs.push(async function prepareDecoder() {
174
+ const existingDecoder = reusable?.runtime.decoderExecutable ?? requirement.decoderExecutable;
175
+ if (existingDecoder) {
176
+ const source = path.resolve(existingDecoder);
177
+ const sourceRoot = path.resolve(reusable?.runtime.decoderRoot ?? requirement.decoderRoot ?? path.dirname(source));
178
+ decoderExecutable = copiedWhisperLocation(sourceRoot, decoderRoot, source);
179
+ await copyWhisperTree(sourceRoot, decoderRoot, signal);
180
+ return;
181
+ }
182
+ await prepareWhisperArchive(
183
+ requirement.decoderUrl ?? 'https://github.com/GyanD/codexffmpeg/releases/download/9.0.2/ffmpeg-9.0.2-essentials_build.zip',
184
+ decoderRoot,
185
+ preparation
186
+ );
187
+ decoderExecutable = await findWhisperFile(decoderRoot, `ffmpeg${platform === 'win32' ? '.exe' : ''}`, signal);
188
+ });
189
+ for (const [index, model] of requirement.models.entries()) {
190
+ jobs.push(async function prepareSelectedModel() {
191
+ const modelRoot = path.join(root, 'models', String(index));
192
+ await mkdir(modelRoot, {recursive: true});
193
+ const filename = model.path ? path.basename(model.path) : model.filename ?? path.posix.basename(new URL(model.url).pathname);
194
+ const destination = path.join(modelRoot, filename);
195
+ const priorIndex = reusable?.requirement.models.findIndex(function sameSelectedModel(candidate) {
196
+ return JSON.stringify(candidate) === JSON.stringify(model);
197
+ }) ?? -1;
198
+ let source = model.path;
199
+ if (priorIndex >= 0) {
200
+ const priorPath = reusable.runtime.models[priorIndex]?.path;
201
+ if (priorPath) {
202
+ try {
203
+ if ((await stat(priorPath)).isFile()) source = priorPath;
204
+ } catch (error) {
205
+ if (error.code !== 'ENOENT') throw error;
206
+ }
207
+ }
208
+ }
209
+ if (source) await copyFile(path.resolve(source), destination);
210
+ else await downloadWhisperResource(model.url, destination, preparation);
211
+ models[index] = {...model, path: destination};
212
+ });
213
+ }
214
+ let nextJob = 0;
215
+ const failures = [];
216
+ async function prepareWhisperDependencies() {
217
+ while (nextJob < jobs.length) {
218
+ throwIfAborted(signal);
219
+ const job = jobs[nextJob++];
220
+ try {
221
+ await job();
222
+ } catch (error) {
223
+ failures.push(error);
224
+ controller.abort(error);
225
+ }
226
+ }
227
+ }
228
+ const workers = await Promise.allSettled(Array.from({length: Math.min(3, jobs.length)}, function startWhisperDependencyWorker() {
229
+ return prepareWhisperDependencies();
230
+ }));
231
+ for (const worker of workers) if (worker.status === 'rejected') failures.push(worker.reason);
232
+ throwIfAborted(options.signal);
233
+ if (failures.length === 1) throw failures[0];
234
+ if (failures.length > 1) throw new AggregateError(failures, 'Whisper dependency preparation failed.');
235
+ const locations = {
236
+ version: requirement.version,
237
+ backend: requirement.backend,
238
+ helperRoot,
239
+ helperExecutable,
240
+ helperVersion: requirement.helperVersion ?? packageMetadata.version,
241
+ decoderRoot,
242
+ decoderExecutable,
243
+ variants: preparedVariants.map(function completedWhisperVariant(variant) {
244
+ return {...variant, executable: helperExecutable};
245
+ }),
246
+ models,
247
+ ...(requirement.modelId ? {modelId: requirement.modelId} : models.length === 1 ? {modelId: models[0].id} : {})
248
+ };
249
+ if (!await installedWhisperRuntime({...locations, root}, signal)) {
250
+ throw new ArcaneError(ERROR_CODES.operationFailed, 'Whisper preparation did not produce the selected helper, decoder, runtime directories, and model files.');
251
+ }
252
+ await onEvent({type: 'local-ai.install.prepared', message: 'Whisper runtime, decoder, helper, and selected models are prepared.', data: {id: 'whisper.cpp'}});
253
+ return locations;
254
+ }
255
+
256
+ export function bundledWhisperRuntime(runtime, relativeRoot) {
257
+ function bundledWhisperPath(location) {
258
+ return path.posix.join(relativeRoot, path.relative(runtime.root, location).split(path.sep).join('/'));
259
+ }
260
+ return {
261
+ ...runtime,
262
+ root: relativeRoot,
263
+ helperRoot: bundledWhisperPath(runtime.helperRoot ?? path.dirname(runtime.helperExecutable)),
264
+ helperExecutable: bundledWhisperPath(runtime.helperExecutable),
265
+ decoderRoot: bundledWhisperPath(runtime.decoderRoot),
266
+ decoderExecutable: bundledWhisperPath(runtime.decoderExecutable),
267
+ variants: runtime.variants.map(function bundledWhisperVariant(variant) {
268
+ return {...variant, root: bundledWhisperPath(variant.root), libraryDirectory: bundledWhisperPath(variant.libraryDirectory), executable: bundledWhisperPath(variant.executable)};
269
+ }),
270
+ models: runtime.models.map(function bundledWhisperModel(model) {
271
+ return {...model, path: bundledWhisperPath(model.path)};
272
+ })
273
+ };
274
+ }
@@ -0,0 +1,98 @@
1
+ cmake_minimum_required(VERSION 3.26)
2
+ project(ArcaneWhisper LANGUAGES CXX)
3
+
4
+ set(WHISPER_SOURCE_DIRECTORY "" CACHE PATH "Matching upstream whisper.cpp source directory")
5
+ set(WHISPER_RUNTIME_DIRECTORY "" CACHE PATH "Selected complete upstream runtime directory")
6
+
7
+ find_path(WHISPER_INCLUDE_DIRECTORY whisper.h
8
+ PATHS "${WHISPER_SOURCE_DIRECTORY}/include" REQUIRED NO_DEFAULT_PATH)
9
+ find_path(GGML_INCLUDE_DIRECTORY ggml-backend.h
10
+ PATHS "${WHISPER_SOURCE_DIRECTORY}/ggml/include" REQUIRED NO_DEFAULT_PATH)
11
+
12
+ function(import_whisper_library name)
13
+ if(WIN32)
14
+ find_file(${name}_DLL "${name}.dll"
15
+ PATHS "${WHISPER_RUNTIME_DIRECTORY}" "${WHISPER_RUNTIME_DIRECTORY}/bin"
16
+ REQUIRED NO_DEFAULT_PATH)
17
+ find_library(${name}_IMPORT_LIBRARY NAMES "${name}"
18
+ PATHS "${WHISPER_RUNTIME_DIRECTORY}" "${WHISPER_RUNTIME_DIRECTORY}/lib"
19
+ NO_DEFAULT_PATH)
20
+ if(NOT ${name}_IMPORT_LIBRARY)
21
+ if(NOT MSVC)
22
+ message(FATAL_ERROR "This upstream Windows runtime needs MSVC to generate its missing import library.")
23
+ endif()
24
+ get_filename_component(compiler_directory "${CMAKE_CXX_COMPILER}" DIRECTORY)
25
+ find_program(DUMPBIN_EXECUTABLE dumpbin HINTS "${compiler_directory}" REQUIRED)
26
+ find_program(LIB_EXECUTABLE lib HINTS "${compiler_directory}" REQUIRED)
27
+ execute_process(COMMAND "${DUMPBIN_EXECUTABLE}" /NOLOGO /EXPORTS "${${name}_DLL}"
28
+ OUTPUT_VARIABLE export_table ERROR_VARIABLE export_error RESULT_VARIABLE export_result)
29
+ if(NOT export_result EQUAL 0)
30
+ message(FATAL_ERROR "${export_table}${export_error}")
31
+ endif()
32
+ file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/${name}-exports.txt" "${export_table}${export_error}")
33
+ string(REGEX MATCHALL "[\r\n][ \t]+[0-9]+[ \t]+[0-9A-Fa-f]+[ \t]+[0-9A-Fa-f]+[ \t]+[^ \t\r\n]+" rows "${export_table}")
34
+ set(definition "LIBRARY ${name}.dll\nEXPORTS\n")
35
+ foreach(row IN LISTS rows)
36
+ string(REGEX REPLACE "^[\r\n][ \t]+[0-9]+[ \t]+[0-9A-Fa-f]+[ \t]+[0-9A-Fa-f]+[ \t]+" "" symbol "${row}")
37
+ string(APPEND definition "${symbol}\n")
38
+ endforeach()
39
+ set(definition_path "${CMAKE_CURRENT_BINARY_DIR}/${name}.def")
40
+ set(${name}_IMPORT_LIBRARY "${CMAKE_CURRENT_BINARY_DIR}/${name}.lib")
41
+ file(WRITE "${definition_path}" "${definition}")
42
+ if(CMAKE_SYSTEM_PROCESSOR MATCHES "[Aa][Rr][Mm]64|aarch64" OR CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64")
43
+ set(import_machine ARM64)
44
+ elseif(CMAKE_SIZEOF_VOID_P EQUAL 8)
45
+ set(import_machine X64)
46
+ else()
47
+ set(import_machine X86)
48
+ endif()
49
+ execute_process(COMMAND "${LIB_EXECUTABLE}" /NOLOGO "/DEF:${definition_path}"
50
+ "/OUT:${${name}_IMPORT_LIBRARY}" "/MACHINE:${import_machine}"
51
+ OUTPUT_VARIABLE import_output ERROR_VARIABLE import_error RESULT_VARIABLE import_result)
52
+ if(NOT import_result EQUAL 0)
53
+ message(FATAL_ERROR "${import_output}${import_error}")
54
+ endif()
55
+ endif()
56
+ add_library(upstream_${name} SHARED IMPORTED)
57
+ set_target_properties(upstream_${name} PROPERTIES
58
+ IMPORTED_LOCATION "${${name}_DLL}"
59
+ IMPORTED_IMPLIB "${${name}_IMPORT_LIBRARY}")
60
+ else()
61
+ find_library(${name}_LIBRARY NAMES "${name}"
62
+ PATHS "${WHISPER_RUNTIME_DIRECTORY}" "${WHISPER_RUNTIME_DIRECTORY}/lib"
63
+ REQUIRED NO_DEFAULT_PATH)
64
+ add_library(upstream_${name} SHARED IMPORTED)
65
+ set_target_properties(upstream_${name} PROPERTIES IMPORTED_LOCATION "${${name}_LIBRARY}")
66
+ endif()
67
+ endfunction()
68
+
69
+ import_whisper_library(whisper)
70
+ import_whisper_library(ggml)
71
+ find_package(Threads REQUIRED)
72
+
73
+ add_executable(arcane-whisper main.cpp)
74
+ target_compile_features(arcane-whisper PRIVATE cxx_std_17)
75
+ target_compile_definitions(arcane-whisper PRIVATE WHISPER_SHARED GGML_SHARED)
76
+ target_include_directories(arcane-whisper PRIVATE "${WHISPER_INCLUDE_DIRECTORY}" "${GGML_INCLUDE_DIRECTORY}")
77
+ target_link_libraries(arcane-whisper PRIVATE upstream_whisper upstream_ggml Threads::Threads)
78
+ if(MSVC)
79
+ target_compile_options(arcane-whisper PRIVATE /utf-8 /EHsc)
80
+ endif()
81
+ if(APPLE)
82
+ set_target_properties(arcane-whisper PROPERTIES INSTALL_RPATH "@loader_path;@loader_path/../lib")
83
+ elseif(UNIX)
84
+ set_target_properties(arcane-whisper PROPERTIES INSTALL_RPATH "$ORIGIN;$ORIGIN/../lib")
85
+ endif()
86
+ install(TARGETS arcane-whisper RUNTIME DESTINATION bin)
87
+
88
+ if(WIN32 AND MSVC)
89
+ # The published Whisper DLLs and this helper share the release MSVC CRT.
90
+ # Keep its permitted app-local redistributables beside the executable.
91
+ set(CMAKE_INSTALL_SYSTEM_RUNTIME_DESTINATION bin)
92
+ set(CMAKE_INSTALL_DEBUG_LIBRARIES FALSE)
93
+ set(CMAKE_INSTALL_DEBUG_LIBRARIES_ONLY FALSE)
94
+ set(CMAKE_INSTALL_UCRT_LIBRARIES FALSE)
95
+ set(CMAKE_INSTALL_MFC_LIBRARIES FALSE)
96
+ set(CMAKE_INSTALL_OPENMP_LIBRARIES TRUE)
97
+ include(InstallRequiredSystemLibraries)
98
+ endif()
@@ -0,0 +1,18 @@
1
+ Arcane Whisper native helper
2
+ Copyright (c) The Wizard Nexus and contributors.
3
+
4
+ The first-party helper source is licensed under AGPL-3.0-only; see LICENSE.
5
+ Matching source and build instructions are included in source/ and in the
6
+ corresponding Arcane SDK numeric release.
7
+
8
+ This Windows helper distribution includes unmodified Microsoft Visual C++
9
+ release runtime libraries from the installed Visual Studio 2022 Redistributable
10
+ tree. Those files retain Microsoft's terms. Their published distribution list
11
+ and application-local deployment documentation are available at:
12
+ https://learn.microsoft.com/en-us/visualstudio/releases/2022/redistribution
13
+ https://learn.microsoft.com/en-us/cpp/windows/redistributing-visual-cpp-files
14
+
15
+ The helper uses the public whisper.cpp and GGML interfaces. Their MIT license
16
+ is included as licenses/whisper-LICENSE. The upstream runtime archives, model
17
+ and FFmpeg decoder are acquired separately through their selected upstream
18
+ distribution routes; they are not included in this helper archive.
@@ -0,0 +1,104 @@
1
+ # Persistent Whisper helper
2
+
3
+ This first-party executable links the public C API from the selected upstream
4
+ whisper.cpp runtime. The current integration selects whisper.cpp 1.9.4/b5130.
5
+ The matching upstream headers and complete runtime remain in their installed
6
+ locations; they are not copied into SDK source. CMake builds only this helper.
7
+
8
+ `buildWhisperHelper` in `../build.mjs` takes the runtime's `directory`, matching
9
+ upstream `sourceDirectory`, and a task-owned `outputRoot`. Optional `cmake`,
10
+ `generator`, `cmakeArgs`, and `env` select an already available host toolchain.
11
+ Its `signal` and `onEvent` use the SDK process owner. The result names the
12
+ helper executable and retains complete configure, build, and installation
13
+ diagnostics. On Windows, when the upstream distribution omits import libraries,
14
+ the selected MSVC `dumpbin` and `lib` generate ordinary import libraries from
15
+ the actual DLL exports. This does not rebuild Whisper, ggml, or CUDA.
16
+ The Windows MSVC installation also uses CMake's
17
+ `InstallRequiredSystemLibraries` module to place the selected release CRT and
18
+ OpenMP redistributables beside the helper in `bin/`. Debug runtimes, MFC, and
19
+ Windows Universal CRT files are excluded; Windows 10/11 supplies the latter.
20
+ An explicit `-DMSVC_REDIST_DIR=...` in `cmakeArgs` can select the permitted
21
+ installed compiler redistributable tree for that build.
22
+
23
+ Windows uses the selected runtime directory in the child process's `PATH`.
24
+ Linux and macOS use their normal library loader configuration for that runtime.
25
+ The helper source uses standard C++17, with Windows binary standard-stream
26
+ mode as its only host-specific source adaptation. Android needs its native host
27
+ to provide the executable, upstream runtime, decoder, and process integration.
28
+ Availability of this source does not establish execution on another platform.
29
+
30
+ ## Launch and commands
31
+
32
+ Launch with these arguments:
33
+
34
+ ```text
35
+ arcane-whisper --model-base64 MODEL_PATH --runtime-base64 RUNTIME_PATH --backend gpu --threads 4
36
+ ```
37
+
38
+ Both path arguments are the base64 encoding of the complete UTF-8 path. The
39
+ backend is `gpu` or `cpu`. The optional thread argument selects native inference
40
+ threads; its default is the host's reported hardware concurrency, or one when
41
+ the host does not report it. The parent keeps runtime library resolution and
42
+ the backend directory aligned with the same selected runtime tree.
43
+
44
+ Each stdin command ends with one LF. The displayed `TAB` means an actual tab:
45
+
46
+ ```text
47
+ transcribe TAB REQUEST_ID TAB PCM_PATH_BASE64 TAB LANGUAGE_BASE64 TAB TRANSLATE
48
+ cancel TAB REQUEST_ID
49
+ shutdown
50
+ ```
51
+
52
+ Request identifiers are parent-generated control identifiers. Paths and
53
+ languages use lossless base64 framing; `TRANSLATE` is `0` or `1`. The language
54
+ is a Whisper language name/code, `auto`, or empty for detection. The PCM file is
55
+ the complete decoded recording in mono 16 kHz f32le format. The decoder owns
56
+ that required representation; the helper adds no trimming, voice-activity
57
+ filter, duration restriction, offset, or audio normalization. The original
58
+ recording remains with the request owner. The native `whisper_full` signed-int
59
+ sample argument is an upstream representational limit; a recording it cannot
60
+ represent produces an explicit error without silently shortening the input.
61
+
62
+ ## Events and ownership
63
+
64
+ Stdout contains complete JSON records, one per LF, each explicitly flushed.
65
+ Native diagnostics remain complete on stderr.
66
+
67
+ | `type` | Fields and meaning |
68
+ | --- | --- |
69
+ | `loading` | `model`, `requestedBackend`; the retained context is being initialized. |
70
+ | `ready` | `model`, `requestedBackend`, `observedBackend`, `backendEvidence`; model initialization and state allocation returned successfully. |
71
+ | `accepted` | `requestId`; this request owns the retained context. |
72
+ | `progress` | `requestId`, `progress`; the actual upstream percentage callback, not an elapsed-time prediction. |
73
+ | `segment` | `requestId`, `index`, `text`, `start`, `end`; an actual newly produced native segment, in original callback order. |
74
+ | `complete` | `requestId`, `language`, `duration`, `text`, `segments`; the full native result after the inference worker joined. |
75
+ | `cancelled` | `requestId`; the cancellation was observed and the actual inference worker joined. |
76
+ | `error` | `requestId`, `message`; an actual operation failure. Startup failures use an empty request identifier. `code: "WHISPER_INFERENCE_FAILED"` identifies only a nonzero return from `whisper_full` after cancellation has been excluded. |
77
+ | `stopped` | The command reader joined and the retained context was freed. Process exit remains the parent's lifetime boundary. |
78
+
79
+ Segment start/end and recording duration are in seconds. Text preserves native
80
+ segment whitespace. Complete text concatenates the native segments in order,
81
+ with no trimming, inserted separators, or rewritten model output. No segment is
82
+ fabricated from tokens or inferred progress. The helper uses upstream greedy
83
+ decoding defaults with independent request context, zero offset, and the full
84
+ decoded duration.
85
+
86
+ The public C API has no context-backend getter. `observedBackend` therefore
87
+ reports the backend identified by completed native initialization logs, with
88
+ `backendEvidence: "runtime-log"`; it is null if those logs provided no usable
89
+ observation. It does not promise that every operation runs on a GPU. GPU
90
+ failure and CPU runtime selection belong to the parent runtime owner.
91
+
92
+ One active inference uses the retained context. A separate command reader can
93
+ set its cancellation flag while computation runs. Both the native encoder and
94
+ compute abort callbacks observe that flag. Native work drains before the main
95
+ dispatcher joins the inference worker and emits a terminal event. Upstream may
96
+ finish an already running GPU operation before observing cancellation.
97
+ Callbacks produced before native cancellation finishes remain complete events;
98
+ the parent owns whether a cancelled operation may affect its visible result.
99
+
100
+ `shutdown` and stdin EOF cancel active inference, join it and the command
101
+ reader, and free the model. Model loading has no public upstream cancellation
102
+ callback, so the parent cancels loading by terminating and joining its exact
103
+ helper process. A different selected model likewise belongs to a new retained
104
+ helper lifetime after the previous owner drains.